Skip to content

redbean: Add LuaCrypto compatible functions (plus some auxiliary functions) #1422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions test/tool/net/lcrypto_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
-- Helper function to print test results
local function assert_equal(actual, expected, message)
if actual ~= expected then
error("FAIL: " .. message .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual))
end
end

local function assert_not_equal(actual, not_expected, message)
if actual == not_expected then
error(message .. ": did not expect " .. tostring(not_expected))
end
end

-- Test RSA key pair generation
local function test_rsa_keypair_generation()
local priv_key, pub_key = crypto.generatekeypair("rsa", 2048)
assert_equal(type(priv_key), "string", "Private key type")
assert_equal(type(pub_key), "string", "Public key type")
end

-- Test ECDSA key pair generation
local function test_ecdsa_keypair_generation()
local priv_key, pub_key = crypto.generatekeypair("ecdsa", "secp256r1")
assert_equal(type(priv_key), "string", "Private key type")
assert_equal(type(pub_key), "string", "Public key type")
end

-- Test RSA encryption and decryption
local function test_rsa_encryption_decryption()
local priv_key, pub_key = crypto.generatekeypair("rsa", 2048)
assert(type(priv_key) == "string", "Private key type")
assert(type(pub_key) == "string", "Public key type")
local plaintext = "Hello, RSA!"
local encrypted = crypto.encrypt("rsa", pub_key, plaintext)
assert_equal(type(encrypted), "string", "Ciphertext type")
local decrypted = crypto.decrypt("rsa", priv_key, encrypted)
assert_equal(decrypted, plaintext, "Decrypted ciphertext matches plaintext")
end

-- Test RSA signing and verification
local function test_rsa_signing_verification()
local priv_key, pub_key = crypto.generatekeypair("rsa", 2048)
assert(type(priv_key) == "string", "Private key type")
assert(type(pub_key) == "string", "Public key type")
local message = "Sign this message"
local signature = crypto.sign("rsa", priv_key, message, "sha256")
assert_equal(type(signature), "string", "Signature type")
local is_valid = crypto.verify("rsa", pub_key, message, signature, "sha256")
assert_equal(is_valid, true, "Signature verification")
end

-- Test ECDSA signing and verification
local function test_ecdsa_signing_verification()
local priv_key, pub_key = crypto.generatekeypair("ecdsa", "secp256r1")
assert(type(priv_key) == "string", "Private key type")
assert(type(pub_key) == "string", "Public key type")
local message = "Sign this message with ECDSA"
local signature = crypto.sign("ecdsa", priv_key, message, "sha256")
assert_equal(type(signature), "string", "Signature type")
local is_valid = crypto.verify("ecdsa", pub_key, message, signature, "sha256")
assert_equal(is_valid, true, "Signature verification")
end

-- Test AES key generation
local function test_aes_key_generation()
local key = crypto.generatekeypair('aes', 256) -- 256-bit key
assert_equal(type(key), "string", "Key type")
assert_equal(#key, 32, "Key length (256 bits)")
end

-- Test AES encryption and decryption (CBC mode)
local function test_aes_encryption_decryption_cbc()
local key = crypto.generatekeypair('aes', 256) -- 256-bit key
local plaintext = "Hello, AES CBC!"

-- Encrypt without providing IV (should auto-generate IV)
local encrypted, iv = crypto.encrypt("aes", key, plaintext, nil)
assert_equal(type(encrypted), "string", "Ciphertext type")
assert_equal(type(iv), "string", "IV type")

-- Decrypt
local decrypted = crypto.decrypt("aes", key, encrypted, {mode="cbc",iv=iv})
assert_equal(decrypted, plaintext, "Decrypted ciphertext matches plaintext")

-- Encrypt with explicit IV
local iv2 = GetRandomBytes(16)
local encrypted2, iv_used = crypto.encrypt("aes", key, plaintext, {mode="cbc",iv=iv2})
assert_equal(type(encrypted2), "string", "Ciphertext type")
assert_equal(iv_used, iv2, "IV match")

local decrypted2 = crypto.decrypt("aes", key, encrypted2, {mode="cbc",iv=iv2})
assert_equal(decrypted2, plaintext, "Decrypted ciphertext matches plaintext")
end

-- Test AES encryption and decryption (CTR mode)
local function test_aes_encryption_decryption_ctr()
local key = crypto.generatekeypair('aes', 256)
local plaintext = "Hello, AES CTR!"

-- Encrypt without providing IV (should auto-generate IV)
local encrypted, iv = crypto.encrypt("aes", key, plaintext, {mode="ctr"})
assert_equal(type(encrypted), "string", "Ciphertext type")
assert_equal(type(iv), "string", "IV type")

-- Decrypt
local decrypted = crypto.decrypt("aes", key, encrypted, {mode="ctr", iv=iv})
assert_equal(decrypted, plaintext, "Decrypted ciphertext matches plaintext")

-- Encrypt with explicit IV
local iv2 = GetRandomBytes(16)
local encrypted2, iv_used = crypto.encrypt("aes", key, plaintext, {mode="ctr", iv=iv2})
assert_equal(type(encrypted2), "string", "Ciphertext type")
assert_equal(iv_used, iv2, "IV match")

local decrypted2 = crypto.decrypt("aes", key, encrypted2, {mode="ctr", iv=iv2})
assert_equal(decrypted2, plaintext, "Decrypted ciphertext matches plaintext")
end

-- Test AES encryption and decryption (GCM mode)
local function test_aes_encryption_decryption_gcm()
local key = crypto.generatekeypair('aes', 256)
assert_equal(type(key), "string", "key type")
local plaintext = "Hello, AES GCM!"

-- Encrypt without providing IV (should auto-generate IV)
local encrypted, iv, tag = crypto.encrypt("aes", key, plaintext, {mode="gcm"})
assert_equal(#plaintext, #encrypted, "Ciphertext length matches plaintext")
assert_equal(type(encrypted), "string", "Ciphertext type")
assert_equal(type(iv), "string", "IV type")
assert_equal(type(tag), "string", "Tag type")

-- Decrypt
local decrypted = crypto.decrypt("aes", key, encrypted, {mode="gcm",iv=iv,tag=tag})
assert_equal(decrypted, plaintext, "Decrypted ciphertext matches plaintext")

-- Encrypt with explicit IV
local iv2 = GetRandomBytes(13) -- GCM IV/nonce can be 12-16 bytes, 12 is standard
local encrypted2, iv_used, tag2 = crypto.encrypt("aes", key, plaintext, {mode="gcm",iv=iv2})
assert_equal(type(encrypted2), "string", "Ciphertext type")
assert_equal(iv_used, iv2, "IV match")
assert_equal(type(tag2), "string", "Tag type")

local decrypted2 = crypto.decrypt("aes", key, encrypted2, {mode="gcm",iv=iv2,tag=tag2})
assert_equal(decrypted2, plaintext, "Decrypted ciphertext matches plaintext")
end

-- Test PemToJwk conversion
local function test_pem_to_jwk()
local priv_key, pub_key = crypto.generatekeypair()
local priv_jwk = crypto.convertPemToJwk(priv_key)
assert_equal(type(priv_jwk), "table", "JWK type")
assert_equal(priv_jwk.kty, "RSA", "kty is correct")

local pub_jwk = crypto.convertPemToJwk(pub_key)
assert_equal(type(pub_jwk), "table", "JWK type")
assert_equal(pub_jwk.kty, "RSA", "kty is correct")

-- Test ECDSA keys
local priv_key, pub_key = crypto.generatekeypair('ecdsa')
local priv_jwk = crypto.convertPemToJwk(priv_key)
assert_equal(type(priv_jwk), "table", "JWK type")
assert_equal(priv_jwk.kty, "EC", "kty is correct")

local pub_jwk = crypto.convertPemToJwk(pub_key)
assert_equal(type(pub_jwk), "table", "JWK type")
assert_equal(pub_jwk.kty, "EC", "kty is correct")
end

-- Test CSR generation
local function test_csr_generation()
local priv_key, _ = crypto.generatekeypair()
local subject_name = "CN=example.com,O=Example Org,C=US"
local san = "DNS:example.com, DNS:www.example.com, IP:192.168.1.1"
assert_equal(type(priv_key), "string", "Private key type")

local csr = crypto.generateCsr(priv_key, subject_name)
assert_equal(type(csr), "string", "CSR generation with subject name")

csr = crypto.generateCsr(priv_key, subject_name, san)
assert_equal(type(csr), "string", "CSR generation with subject name and san")

csr = crypto.generateCsr(priv_key, nil, san)
assert_equal(type(csr), "string", "CSR generation with nil subject name and san")

csr = crypto.generateCsr(priv_key, '', san)
assert_equal(type(csr), "string", "CSR generation with empty subject name and san")

-- These should fail
csr = crypto.generateCsr(priv_key, '')
assert_not_equal(type(csr), "string", "CSR generation with empty subject name and no san is rejected")

csr = crypto.generateCsr(priv_key)
assert_not_equal(type(csr), "string", "CSR generation with nil subject name and no san is rejected")
end

-- Run all tests
local function run_tests()
test_rsa_keypair_generation()
test_rsa_signing_verification()
test_rsa_encryption_decryption()
test_ecdsa_keypair_generation()
test_ecdsa_signing_verification()
test_aes_key_generation()
test_aes_encryption_decryption_cbc()
test_aes_encryption_decryption_ctr()
test_aes_encryption_decryption_gcm()
test_pem_to_jwk()
test_csr_generation()
EXIT = 0
return EXIT
end

EXIT = 70
os.exit(run_tests())
6 changes: 3 additions & 3 deletions third_party/mbedtls/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@

/* block modes */
#define MBEDTLS_GCM_C
#ifndef TINY
#define MBEDTLS_CIPHER_MODE_CBC
#define MBEDTLS_CIPHER_MODE_CTR
#ifndef TINY
/*#define MBEDTLS_CCM_C*/
/*#define MBEDTLS_CIPHER_MODE_CFB*/
/*#define MBEDTLS_CIPHER_MODE_CTR*/
/*#define MBEDTLS_CIPHER_MODE_OFB*/
/*#define MBEDTLS_CIPHER_MODE_XTS*/
#endif
Expand Down Expand Up @@ -71,10 +71,10 @@
/* eliptic curves */
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_DP_SECP384R1_ENABLED
#define MBEDTLS_ECP_DP_SECP521R1_ENABLED
#define MBEDTLS_ECP_DP_CURVE25519_ENABLED
#ifndef TINY
#define MBEDTLS_ECP_DP_CURVE448_ENABLED
/*#define MBEDTLS_ECP_DP_SECP521R1_ENABLED*/
/*#define MBEDTLS_ECP_DP_BP384R1_ENABLED*/
/*#define MBEDTLS_ECP_DP_SECP192R1_ENABLED*/
/*#define MBEDTLS_ECP_DP_SECP224R1_ENABLED*/
Expand Down
1 change: 1 addition & 0 deletions tool/net/BUILD.mk
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ TOOL_NET_REDBEAN_LUA_MODULES = \
o/$(MODE)/tool/net/lmaxmind.o \
o/$(MODE)/tool/net/lsqlite3.o \
o/$(MODE)/tool/net/largon2.o \
o/$(MODE)/tool/net/lcrypto.o \
o/$(MODE)/tool/net/launch.o

o/$(MODE)/tool/net/redbean.dbg: \
Expand Down
67 changes: 67 additions & 0 deletions tool/net/definitions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8048,6 +8048,73 @@ kUrlPlus = nil
---@type integer to transcode ISO-8859-1 input into UTF-8. See `ParseUrl`.
kUrlLatin1 = nil


--- This module provides cryptographic operations.

--- The crypto module for cryptographic operations
crypto = {}

--- Converts a PEM-encoded key to JWK format
---@param pem string PEM-encoded key
---@return table?, string? JWK table or nil on error
---@return string? error message
function crypto.convertPemToJwk(pem) end

--- Generates a Certificate Signing Request (CSR)
---@param key_pem string PEM-encoded private key
---@param subject_name string? X.509 subject name
---@param san_list string? Subject Alternative Names
---@return string?, string? CSR in PEM format or nil on error and error message
function crypto.generateCsr(key_pem, subject_name, san_list) end

--- Signs data using a private key
---@param key_type string "rsa" or "ecdsa"
---@param private_key string PEM-encoded private key
---@param message string Data to sign
---@param hash_algo string? Hash algorithm (default: SHA-256)
---@return string?, string? Signature or nil on error and error message
function crypto.sign(key_type, private_key, message, hash_algo) end

--- Verifies a signature
---@param key_type string "rsa" or "ecdsa"
---@param public_key string PEM-encoded public key
---@param message string Original message
---@param signature string Signature to verify
---@param hash_algo string? Hash algorithm (default: SHA-256)
---@return boolean?, string? True if valid or nil on error and error message
function crypto.verify(key_type, public_key, message, signature, hash_algo) end

--- Encrypts data
---@param cipher_type string "rsa" or "aes"
---@param key string Public key or symmetric key
---@param plaintext string Data to encrypt
---@param mode string? AES mode: "cbc", "gcm", "ctr" (default: "cbc")
---@param iv string? Initialization Vector for AES
---@param aad string? Additional data for AES-GCM
---@return string? Encrypted data or nil on error
---@return string? IV or error message
---@return string? Authentication tag for GCM mode
function crypto.encrypt(cipher_type, key, plaintext, mode, iv, aad) end

--- Decrypts data
---@param cipher_type string "rsa" or "aes"
---@param key string Private key or symmetric key
---@param ciphertext string Data to decrypt
---@param iv string? Initialization Vector for AES
---@param mode string? AES mode: "cbc", "gcm", "ctr" (default: "cbc")
---@param tag string? Authentication tag for AES-GCM
---@param aad string? Additional data for AES-GCM
---@return string?, string? Decrypted data or nil on error and error message
function crypto.decrypt(cipher_type, key, ciphertext, iv, mode, tag, aad) end

--- Generates cryptographic keys
---@param key_type string? "rsa", "ecdsa", or "aes"
---@param key_size_or_curve number|string? Key size or curve name
---@return string? Private key or nil on error
---@return string? Public key (nil for AES) or error message
function crypto.generatekeypair(key_type, key_size_or_curve) end


--[[
────────────────────────────────────────────────────────────────────────────────
LEGAL
Expand Down
Loading