diff --git a/test/tool/net/lcrypto_test.lua b/test/tool/net/lcrypto_test.lua index 27d4fb3fb..d21321e4b 100644 --- a/test/tool/net/lcrypto_test.lua +++ b/test/tool/net/lcrypto_test.lua @@ -1,4 +1,3 @@ ----@diagnostic disable: lowercase-global -- Test RSA key pair generation local function test_rsa_keypair_generation() local priv_key, pub_key = crypto.generateKeyPair("rsa", 2048) @@ -678,14 +677,14 @@ end local function run_tests() Log(kLogVerbose,"Testing RSA keypair generation...") test_rsa_keypair_generation() - Log(kLogVerbose,"Testing RSA signing and verification...") - test_rsa_signing_verification() - Log(kLogVerbose,"Testing RSA-PSS signing and verification...") - test_rsapss_signing_verification() - Log(kLogVerbose,"Testing RSA encryption and decryption...") - test_rsa_encryption_decryption() Log(kLogVerbose,"Testing RSA key size variations...") test_rsa_key_sizes() + Log(kLogVerbose,"Testing RSA signing and verification...") + test_rsa_signing_verification() + Log(kLogVerbose,"Testing RSA encryption and decryption...") + test_rsa_encryption_decryption() + Log(kLogVerbose,"Testing RSA-PSS signing and verification...") + test_rsapss_signing_verification() Log(kLogVerbose,"Testing ECDSA keypair generation...") test_ecdsa_keypair_generation() @@ -753,4 +752,5 @@ local function run_tests() end EXIT = 70 + os.exit(run_tests()) diff --git a/tool/net/definitions.lua b/tool/net/definitions.lua index c21ad82cc..11e545a60 100644 --- a/tool/net/definitions.lua +++ b/tool/net/definitions.lua @@ -746,7 +746,7 @@ function EscapeHtml(str) end ---@param path string? function LaunchBrowser(path) end ----@param ip uint32 +---@param ip integer|string ---@return string # a string describing the IP address. This is currently Class A granular. It can tell you if traffic originated from private networks, ARIN, APNIC, DOD, etc. ---@nodiscard function CategorizeIp(ip) end @@ -1142,10 +1142,10 @@ function FormatHttpDateTime(seconds) end --- Turns integer like `0x01020304` into a string like `"1.2.3.4"`. See also --- `ParseIp` for the inverse operation. ----@param uint32 integer +---@param ip integer ---@return string ---@nodiscard -function FormatIp(uint32) end +function FormatIp(ip) end --- Returns client ip4 address and port, e.g. `0x01020304`,`31337` would represent --- `1.2.3.4:31337`. This is the same as `GetClientAddr` except it will use the @@ -1363,25 +1363,25 @@ function HidePath(prefix) end ---@nodiscard function IsHiddenPath(path) end ----@param uint32 integer +---@param ip integer|string|string ---@return boolean # `true` if IP address is not a private network (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) and is not localhost (`127.0.0.0/8`). --- Note: we intentionally regard TEST-NET IPs as public. ---@nodiscard -function IsPublicIp(uint32) end +function IsPublicIp(ip) end ----@param uint32 integer +---@param ip integer|string|string ---@return boolean # `true` if IP address is part of a private network (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). ---@nodiscard -function IsPrivateIp(uint32) end +function IsPrivateIp(ip) end ---@return boolean # `true` if the client IP address (returned by GetRemoteAddr) is part of the localhost network (127.0.0.0/8). ---@nodiscard function IsLoopbackClient() end ----@param uint32 integer +---@param ip integer|string|string ---@return boolean # true if IP address is part of the localhost network (127.0.0.0/8). ---@nodiscard -function IsLoopbackIp(uint32) end +function IsLoopbackIp(ip) end ---@param path string ---@return boolean # `true` if ZIP artifact at path is stored on disk using DEFLATE compression. @@ -1615,7 +1615,7 @@ function GetCryptoHash(name, payload, key) end --- to the system-configured DNS resolution service. Please note that in MODE=tiny --- the HOSTS.TXT and DNS resolution isn't included, and therefore an IP must be --- provided. ----@param ip integer +---@param ip integer|string|string ---@overload fun(host:string) function ProgramAddr(ip) end @@ -1669,8 +1669,8 @@ function ProgramTimeout(milliseconds) end --- Hard-codes the port number on which to listen, which can be any number in the --- range `1..65535`, or alternatively `0` to ask the operating system to choose a --- port, which may be revealed later on by `GetServerAddr` or the `-z` flag to stdout. ----@param uint16 integer -function ProgramPort(uint16) end +---@param port integer +function ProgramPort(port) end --- Sets the maximum HTTP message payload size in bytes. The --- default is very conservatively set to 65536 so this is @@ -2169,7 +2169,7 @@ function bin(int) end --- unspecified format describing the error. Calls to this function may be wrapped --- in `assert()` if an exception is desired. ---@param hostname string ----@return uint32 ip uint32 +---@return string ---@nodiscard ---@overload fun(hostname: string): nil, error: string function ResolveIp(hostname) end @@ -2183,7 +2183,7 @@ function ResolveIp(hostname) end --- The network interface addresses used by the host machine are always --- considered trustworthy, e.g. 127.0.0.1. This may change soon, if we --- decide to export a `GetHostIps()` API which queries your NIC devices. ----@param ip integer +---@param ip integer|string ---@return boolean function IsTrustedIp(ip) end @@ -2213,7 +2213,7 @@ function IsTrustedIp(ip) end --- --- Although you might want consider trusting redbean's open source --- freedom embracing solution to DDOS protection instead! ----@param ip integer +---@param ip integer|string ---@param cidr integer? function ProgramTrustedIp(ip, cidr) end @@ -8051,85 +8051,70 @@ kUrlLatin1 = nil --- This module provides cryptographic operations. +--- The crypto module for cryptographic operations crypto = {} ---- Signs a message using the specified key type. ---- Supported types: "rsa", "rsa-pss", "ecdsa" ----@param type "rsa"|"rsa-pss"|"rsapss"|"ecdsa" ----@param key string PEM-encoded private key ----@param message string ----@param hash? string Hash algorithm ("sha256", "sha384", "sha512"). Default: "sha256" ----@return string signature ----@overload fun(type: string, key: string, message: string, hash?: string): nil, error: string -function crypto.sign(type, key, message, hash) end +--- 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 ---- Verifies a signature using the specified key type. ---- Supported types: "rsa", "rsa-pss", "ecdsa" ----@param type "rsa"|"rsa-pss"|"rsapss"|"ecdsa" ----@param key string PEM-encoded public key ----@param message string ----@param signature string ----@param hash? string Hash algorithm ("sha256", "sha384", "sha512"). Default: "sha256" ----@return boolean valid -function crypto.verify(type, key, message, signature, hash) 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 ---- Encrypts data using the specified cipher. ---- Supported ciphers: "rsa", "aes" ----@param cipher "rsa"|"aes" ----@param key string PEM-encoded public key (RSA) or raw key (AES) ----@param plaintext string ----@param options? table Options for AES: { mode="cbc"|"gcm"|"ctr", iv=string, aad=string } ----@return string ciphertext, string? iv, string? tag ----@overload fun(cipher: string, key: string, plaintext: string, options?: table): nil, error: string -function crypto.encrypt(cipher, key, plaintext, options) 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 ---- Decrypts data using the specified cipher. ---- Supported ciphers: "rsa", "aes" ----@param cipher "rsa"|"aes" ----@param key string PEM-encoded private key (RSA) or raw key (AES) ----@param ciphertext string ----@param options? table Options for AES: { mode="cbc"|"gcm"|"ctr", iv=string, tag=string, aad=string } ----@return string plaintext ----@overload fun(cipher: string, key: string, ciphertext: string, options?: table): nil, error: string -function crypto.decrypt(cipher, key, ciphertext, options) 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 ---- Generates a key pair. ---- For RSA: bits = 2048 or 4096. ---- For ECDSA: curve = "secp256r1", "secp384r1", "secp521r1", "curve25519" ---- For AES: bits = 128, 192, or 256. ----@param type "rsa"|"ecdsa"|"aes" ----@param param? integer|string For RSA: bits; for ECDSA: curve name; for AES: bits ----@return string private_key, string public_key|nil ----@overload fun(type: string, param?: integer|string): nil, error: string -function crypto.generateKeyPair(type, param) 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 options table Table with optional parameters: +--- options.mode string? AES mode: "cbc", "gcm", "ctr" (default: "cbc") +--- options.iv string? Initialization Vector for AES +--- options.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, options) end ---- Converts a JWK (JSON Web Key, as a Lua table) to PEM format. ----@param jwk table ----@return string pem ----@overload fun(jwk: table): nil, error: string -function crypto.convertJwkToPem(jwk) 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 options table Table with optional parameters: +--- options.iv string? Initialization Vector for AES +--- options.mode string? AES mode: "cbc", "gcm", "ctr" (default: "cbc") +--- options.tag string? Authentication tag for AES-GCM +--- options.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, options) end ---- Converts a PEM key to JWK (JSON Web Key, as a Lua table). ----@param pem string ----@param claims? table Additional claims to merge (RFC7517 fields) ----@return table jwk ----@overload fun(pem: string, claims?: table): nil, error: string -function crypto.convertPemToJwk(pem, claims) end - ---- Generates a Certificate Signing Request (CSR) from a PEM key. ----@param key string PEM-encoded private key ----@param subject string Subject name (e.g., "CN=example.com") ----@param sans? string Subject Alternative Names (SANs) as a comma-separated string ----@return string csr_pem ----@overload fun(key: string, subject: string, sans?: string): nil, error: string -function crypto.generateCsr(key, subject, sans) end - --- AES options table for encrypt/decrypt: ----@class CryptoAesOptions ----@field mode? "cbc"|"gcm"|"ctr" ----@field iv? string ----@field tag? string ----@field aad? string +--- 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 --[[ diff --git a/tool/net/lcrypto.c b/tool/net/lcrypto.c index 6e4ec032e..f97920893 100644 --- a/tool/net/lcrypto.c +++ b/tool/net/lcrypto.c @@ -50,9 +50,7 @@ static const curve_map_t supported_curves[] = { {"P521", MBEDTLS_ECP_DP_SECP521R1}, // {"P-521", MBEDTLS_ECP_DP_SECP521R1}, // {"curve25519", MBEDTLS_ECP_DP_CURVE25519}, // -#ifndef TINY - {"curve448", MBEDTLS_ECP_DP_CURVE448}, // -#endif + {"curve448", MBEDTLS_ECP_DP_CURVE448}, // {NULL, 0}}; // List available curves @@ -198,6 +196,24 @@ cleanup: return ret; } +// Ciphers +typedef struct { + const char *name; + mbedtls_cipher_id_t id; +} ciphers_map_t; + +static const ciphers_map_t supported_ciphers[] = { + {"AES-128-CBC", MBEDTLS_CIPHER_AES_128_CBC}, // + {"AES-192-CBC", MBEDTLS_CIPHER_AES_192_CBC}, // + {"AES-256-CBC", MBEDTLS_CIPHER_AES_256_CBC}, // + {"AES-128-CTR", MBEDTLS_CIPHER_AES_128_CTR}, // + {"AES-192-CTR", MBEDTLS_CIPHER_AES_192_CTR}, // + {"AES-256-CTR", MBEDTLS_CIPHER_AES_256_CTR}, // + {"AES-128-GCM", MBEDTLS_CIPHER_AES_128_GCM}, // + {"AES-192-GCM", MBEDTLS_CIPHER_AES_192_GCM}, // + {"AES-256-GCM", MBEDTLS_CIPHER_AES_256_GCM}, // + {NULL, 0}}; + // Strong RNG using mbedtls_entropy_context and mbedtls_ctr_drbg_context int GenerateRandom(void *ctx, unsigned char *output, size_t len) { static mbedtls_entropy_context entropy; @@ -2350,14 +2366,10 @@ static int LuaCryptoSign(lua_State *L) { if (strcasecmp(dtype, "rsa") == 0) { return LuaRSASign(L); - } -#ifndef TINY - else if (strcasecmp(dtype, "rsa-pss") == 0 || - strcasecmp(dtype, "rsapss") == 0) { + } else if (strcasecmp(dtype, "rsa-pss") == 0 || + strcasecmp(dtype, "rsapss") == 0) { return LuaRSAPSSSign(L); - } -#endif - else if (strcasecmp(dtype, "ecdsa") == 0) { + } else if (strcasecmp(dtype, "ecdsa") == 0) { return LuaECDSASign(L); } else { return luaL_error(L, "Unsupported signature type: %s", dtype); @@ -2433,6 +2445,99 @@ static int LuaCryptoGenerateKeyPair(lua_State *L) { } } +// Returns a Lua table array of supported digests and ciphers (strings), +// depending on the type argument: +// "ciphers" - returns list of ciphers supported by crypto.encrypt and +// crypto.decrypt "digests" - returns list of digests in supported_digests +// "curves" - returns list of curves in supported_curves +// If no argument is provided, returns a table with all three types +static int LuaList(lua_State *L) { + // Create a new table to hold the result + lua_newtable(L); + + // No argument provided - return all types in a structured table + if (lua_isnoneornil(L, 1)) { + // Create subtable for digests + lua_pushstring(L, "digests"); + lua_newtable(L); + const digest_map_t *digest = supported_digests; + int i = 1; + while (digest->name != NULL) { + lua_pushstring(L, digest->name); + lua_rawseti(L, -2, i++); + digest++; + } + lua_settable(L, -3); + + // Create subtable for curves + lua_pushstring(L, "curves"); + lua_newtable(L); + const curve_map_t *curve = supported_curves; + i = 1; + while (curve->name != NULL) { + lua_pushstring(L, curve->name); + lua_rawseti(L, -2, i++); + curve++; + } + lua_settable(L, -3); + + // Create subtable for ciphers + lua_pushstring(L, "ciphers"); + lua_newtable(L); + const ciphers_map_t *cipher = supported_ciphers; + i = 1; + while (cipher->name != NULL) { + lua_pushstring(L, cipher->name); + lua_rawseti(L, -2, i++); + cipher++; + } + lua_settable(L, -3); + + return 1; + } + + // Argument provided - handle specific type + const char *type = luaL_checkstring(L, 1); + + if (strcasecmp(type, "curves") == 0) { + // List all available curves + const curve_map_t *curve = supported_curves; + int i = 1; + + while (curve->name != NULL) { + lua_pushstring(L, curve->name); + lua_rawseti(L, -2, i++); + curve++; + } + } else if (strcasecmp(type, "digests") == 0) { + // List all available digests + const digest_map_t *digest = supported_digests; + int i = 1; + + while (digest->name != NULL) { + lua_pushstring(L, digest->name); + lua_rawseti(L, -2, i++); + digest++; + } + } else if (strcasecmp(type, "ciphers") == 0) { + // List all available ciphers + const ciphers_map_t *cipher = supported_ciphers; + int i = 1; + + while (cipher->name != NULL) { + lua_pushstring(L, cipher->name); + lua_rawseti(L, -2, i++); + cipher++; + } + } else { + // Invalid type, return empty table + lua_pushstring(L, "Invalid type. Use 'ciphers', 'digests', or 'curves'"); + lua_setfield(L, -2, "error"); + } + + return 1; // Return the table +} + static const luaL_Reg kLuaCrypto[] = { {"sign", LuaCryptoSign}, // {"verify", LuaCryptoVerify}, // @@ -2442,6 +2547,7 @@ static const luaL_Reg kLuaCrypto[] = { {"convertJwkToPem", LuaConvertJwkToPem}, // {"convertPemToJwk", LuaConvertPemToJwk}, // {"generateCsr", LuaGenerateCSR}, // + {"list", LuaList}, // {0}, // };