Add bounded structured crypto operations

This commit is contained in:
udo
2026-07-22 13:43:21 +00:00
parent 68b73343a2
commit 3d155203bd
15 changed files with 564 additions and 0 deletions
+14
View File
@@ -745,6 +745,20 @@ if(valid && password_needs_rehash(encoded))
`password_hash()` returns a self-contained `$uce$scrypt$...` encoding with a random 16-byte salt and the bounded scrypt parameters `N=65536`, `r=8`, `p=1`. `password_verify()` accepts only structurally valid encodings with bounded cost parameters and compares the derived key in constant time. `password_needs_rehash()` reports malformed, legacy, or non-current parameters so applications can upgrade a credential after a successful legacy verification. Treat an empty hash as an operational failure and never store it. Application-level password length policy, rate limiting, and legacy-format verification remain the application's responsibility.
## Structured cryptographic operations
`crypto_operation(request)` is the bounded, algorithm-selected API for structured
asymmetric operations. The initial allowlist supports `operation=key_generate`
and `operation=jwt_sign` with `algorithm=ES256`. Key generation returns P-256
public/private JWKs plus the RFC 7638 thumbprint (`kid`). JWT signing accepts only
a consistent P-256 private JWK, forces `alg=ES256`, and emits a compact JWT with
a 64-byte JOSE signature. Unknown operations and algorithms fail closed.
Existing typed digest, HMAC, password, randomness, and constant-time comparison
functions remain separate. `crypto_operation()` exposes no raw signing, arbitrary
digest/curve selection, encryption, network fetch, or token exchange. Store
private JWKs as secrets.
## Operational footguns
- Keep the FastCGI socket path consistent: `FCGI_SOCKET_PATH` and the web-server `fastcgi_pass` must match exactly. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
+1
View File
@@ -39,6 +39,7 @@ PUBLIC_APIS = [
("hmac_sha256", True, "public"), ("hmac_sha256_hex", True, "public"), ("random_bytes", True, "public"),
("crypto_equal", True, "public"), ("password_hash", True, "public"),
("password_verify", True, "public"), ("password_needs_rehash", True, "public"),
("crypto_operation", True, "public"),
("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
("draw_int", True, "public"), ("draw_float", True, "public"),
+1
View File
@@ -77,6 +77,7 @@ if [[ "$action" == "run" ]]; then
scripts/test_component_once_prefetch.sh
scripts/test_relative_component_cache.sh
scripts/test_password_hashing.sh
scripts/test_crypto_operation_native.sh
scripts/test_mysql_epoch_refresh.sh
scripts/test_mysql_persistent_pool.sh
scripts/test_mysql_persistent_pool_idle.sh
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_source="/tmp/uce-oauth-es256-native-$$.cpp"
test_binary="/tmp/uce-oauth-es256-native-$$"
cleanup() { rm -f "$test_source" "$test_binary"; }
trap cleanup EXIT
cat >"$test_source" <<'EOF'
#include "src/lib/types.cpp"
#include "src/lib/dvalue.cpp"
#include "src/lib/functionlib.cpp"
#include <openssl/bn.h>
#include <openssl/core_names.h>
#include <openssl/ecdsa.h>
#include <openssl/evp.h>
#include <openssl/params.h>
#include <memory>
String base64_encode(String raw)
{
if(raw.empty()) return("");
String out(4 * ((raw.size() + 2) / 3), 0);
int size = EVP_EncodeBlock((unsigned char*)out.data(), (const unsigned char*)raw.data(), (int)raw.size());
out.resize(size > 0 ? (size_t)size : 0);
return(out);
}
String base64_decode(String raw, bool& ok)
{
ok = false;
if(raw.empty() || raw.size() % 4) return("");
String out(3 * raw.size() / 4, 0);
int size = EVP_DecodeBlock((unsigned char*)out.data(), (const unsigned char*)raw.data(), (int)raw.size());
if(size < 0) return("");
while(!raw.empty() && raw.back() == '=') { size--; raw.pop_back(); }
out.resize((size_t)size); ok = true; return(out);
}
#include "src/lib/hash.cpp"
static String b64url_decode(String text)
{
text = replace(replace(text, "-", "+"), "_", "/");
while(text.size() % 4) text += "=";
bool ok = false;
String result = base64_decode(text, ok);
return(ok ? result : String(""));
}
static bool verify(DValue public_jwk, String jwt)
{
StringList parts = split(jwt, ".");
if(parts.size() != 3) return(false);
String x = b64url_decode(public_jwk["x"].to_string());
String y = b64url_decode(public_jwk["y"].to_string());
String raw = b64url_decode(parts[2]);
if(x.size() != 32 || y.size() != 32 || raw.size() != 64) return(false);
unsigned char point[65] = {4}; memcpy(point + 1, x.data(), 32); memcpy(point + 33, y.data(), 32);
OSSL_PARAM params[] = { OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, (char*)"prime256v1", 0), OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_PUB_KEY, point, sizeof(point)), OSSL_PARAM_construct_end() };
EVP_PKEY_CTX* build = EVP_PKEY_CTX_new_from_name(0, "EC", 0); EVP_PKEY* key = 0;
if(!build || EVP_PKEY_fromdata_init(build) <= 0 || EVP_PKEY_fromdata(build, &key, EVP_PKEY_PUBLIC_KEY, params) <= 0) { EVP_PKEY_CTX_free(build); return(false); }
EVP_PKEY_CTX_free(build);
BIGNUM* r = BN_bin2bn((const unsigned char*)raw.data(), 32, 0); BIGNUM* s = BN_bin2bn((const unsigned char*)raw.data() + 32, 32, 0);
ECDSA_SIG* sig = ECDSA_SIG_new(); int der_size = r && s && sig && ECDSA_SIG_set0(sig, r, s) ? i2d_ECDSA_SIG(sig, 0) : 0; r = s = 0;
String der(der_size > 0 ? (size_t)der_size : 0, 0); unsigned char* out = (unsigned char*)der.data();
bool ok = der_size > 0 && i2d_ECDSA_SIG(sig, &out) == der_size;
EVP_MD_CTX* verify_ctx = EVP_MD_CTX_new();
String signing_input = parts[0] + "." + parts[1];
ok = ok && verify_ctx && EVP_DigestVerifyInit(verify_ctx, 0, EVP_sha256(), 0, key) > 0 && EVP_DigestVerify(verify_ctx, (const unsigned char*)der.data(), der.size(), (const unsigned char*)signing_input.data(), signing_input.size()) == 1;
EVP_MD_CTX_free(verify_ctx); ECDSA_SIG_free(sig); EVP_PKEY_free(key); return(ok);
}
int main()
{
DValue key_request; key_request["operation"] = "key_generate"; key_request["algorithm"] = "ES256";
DValue key = crypto_operation_native(key_request);
DValue header; header["alg"] = "none"; header["kid"] = key["kid"]; DValue claims; claims["iss"] = "https://client.example";
auto sign = [&](DValue private_jwk) { DValue request; request["operation"] = "jwt_sign"; request["algorithm"] = "ES256"; request["private_jwk"] = private_jwk; request["protected_header"] = header; request["claims"] = claims; return(crypto_operation_native(request)); };
DValue signed_result = sign(key["private_jwk"]); String jwt = signed_result["jwt"].to_string();
DValue wrong_curve = key["private_jwk"]; wrong_curve["crv"] = "P-384";
DValue malformed = key["private_jwk"]; malformed["x"] = "bad=";
DValue mismatch = key["private_jwk"]; String d = mismatch["d"].to_string(); d[0] = d[0] == 'A' ? 'B' : 'A'; mismatch["d"] = d;
DValue unsupported; unsupported["operation"] = "encrypt"; unsupported["algorithm"] = "ES256";
DValue unknown_algorithm; unknown_algorithm["operation"] = "key_generate"; unknown_algorithm["algorithm"] = "none";
DValue untyped_algorithm = key_request; untyped_algorithm["algorithm"] = (f64)256;
DValue list_header; list_header.set_array(); DValue list_item; list_item = "not-an-object"; list_header.push(list_item); DValue list_request; list_request["operation"] = "jwt_sign"; list_request["algorithm"] = "ES256"; list_request["private_jwk"] = key["private_jwk"]; list_request["protected_header"] = list_header; list_request["claims"] = claims;
DValue control_request = list_request; control_request["protected_header"] = header; control_request["claims"] = claims; control_request["claims"]["bad"] = String("control\nbyte");
DValue oversized = key_request; oversized["ignored"] = String(17000, 'x');
DValue nonfinite = key_request; nonfinite["ignored"] = std::numeric_limits<f64>::quiet_NaN();
String tampered = jwt; if(!tampered.empty()) tampered[tampered.size() - 1] = tampered.back() == 'A' ? 'B' : 'A';
bool kid_ok = key["ok"].to_bool() && key["kid"].to_string() == key["thumbprint"].to_string();
bool signed_ok = signed_result["ok"].to_bool() && jwt != "" && verify(key["public_jwk"], jwt);
bool tamper_ok = !verify(key["public_jwk"], tampered);
bool negatives_ok = !sign(wrong_curve)["ok"].to_bool() && !sign(malformed)["ok"].to_bool() && !sign(mismatch)["ok"].to_bool() && crypto_operation_native(unsupported)["error"].to_string() == "unsupported_operation" && crypto_operation_native(unknown_algorithm)["error"].to_string() == "unsupported_algorithm" && crypto_operation_native(untyped_algorithm)["error"].to_string() == "invalid_request" && crypto_operation_native(list_request)["error"].to_string() == "invalid_key_or_payload" && crypto_operation_native(control_request)["error"].to_string() == "invalid_request" && crypto_operation_native(oversized)["error"].to_string() == "invalid_request" && crypto_operation_native(nonfinite)["error"].to_string() == "invalid_request";
if(!(kid_ok && signed_ok && tamper_ok && negatives_ok)) std::cerr << "kid=" << kid_ok << " signed=" << signed_ok << " tamper=" << tamper_ok << " negatives=" << negatives_ok << " jwt_size=" << jwt.size() << "\\n";
return(kid_ok && signed_ok && tamper_ok && negatives_ok ? 0 : 1);
}
EOF
clang++ -std=c++20 -fpermissive -I. "$test_source" -lpcre2-8 -lcrypto -o "$test_binary"
"$test_binary"
echo "native structured crypto operation passed"
+1
View File
@@ -14,6 +14,7 @@ hmac_sha256
hmac_sha256_hex
random_bytes
crypto_equal
crypto_operation
password_hash
password_verify
password_needs_rehash
+28
View File
@@ -0,0 +1,28 @@
:sig
DValue crypto_operation(DValue request)
:params
request : structured operation, algorithm, and operation-specific fields
return value : map with ok, bounded error code, and operation-specific output
:content
Runs one explicitly supported structured asymmetric cryptographic operation. The initial allowlist is `key_generate` with `ES256` and `jwt_sign` with `ES256`. Unknown operations and algorithms fail closed.
ES256 signing validates that `x`, `y`, and `d` form one P-256 key, forces the protected `alg` to `ES256`, and emits a compact JWT with a 64-byte JOSE signature. Requests, nesting, values, and output are bounded. Header and claims roots must be JSON objects containing valid UTF-8 without raw control bytes.
This function does not replace typed digest, HMAC, password, randomness, or constant-time comparison APIs. It exposes no raw signing, arbitrary curve/digest selection, encryption, or generic OpenSSL access. Keep returned private JWKs secret.
:example
DValue key_request;
key_request["operation"] = "key_generate";
key_request["algorithm"] = "ES256";
DValue key = crypto_operation(key_request);
DValue sign_request;
sign_request["operation"] = "jwt_sign";
sign_request["algorithm"] = "ES256";
sign_request["private_jwk"] = key["private_jwk"];
sign_request["protected_header"]["typ"] = "JWT";
sign_request["claims"]["iss"] = "https://client.example";
DValue signed_jwt = crypto_operation(sign_request);
print(signed_jwt["ok"].to_bool() ? "signed" : "failed", "\n");
+61
View File
@@ -0,0 +1,61 @@
#include "testlib.h"
RENDER(Request& context)
{
u64 passed = 0;
u64 failed = 0;
u64 skipped = 0;
auto check = [&](String name, bool ok, String detail)
{
site_tests_case(name, ok ? "pass" : "fail", detail);
if(ok) passed++; else failed++;
};
site_tests_page_start("Structured crypto operations", "Algorithm-selected P-256 JWK creation and ES256 JWT signing.");
DValue key_request;
key_request["operation"] = "key_generate";
key_request["algorithm"] = "ES256";
DValue key = crypto_operation(key_request);
DValue header;
header["alg"] = "none";
header["kid"] = key["kid"];
DValue claims;
claims["iss"] = "https://client.example";
claims["sub"] = "client.example";
DValue sign_request;
sign_request["operation"] = "jwt_sign";
sign_request["algorithm"] = "ES256";
sign_request["private_jwk"] = key["private_jwk"];
sign_request["protected_header"] = header;
sign_request["claims"] = claims;
DValue signed_result = crypto_operation(sign_request);
String jwt = signed_result["jwt"].to_string();
StringList parts = split(jwt, ".");
String header_b64 = parts.size() == 3 ? parts[0] : "";
while(header_b64.size() % 4) header_b64 += "=";
String decoded_header = base64_decode(header_b64);
DValue wrong_curve = key["private_jwk"];
wrong_curve["crv"] = "P-384";
DValue malformed = key["private_jwk"];
malformed["x"] = "not_base64url=";
DValue wrong_request = sign_request;
wrong_request["private_jwk"] = wrong_curve;
DValue malformed_request = sign_request;
malformed_request["private_jwk"] = malformed;
DValue unsupported;
unsupported["operation"] = "encrypt";
unsupported["algorithm"] = "ES256";
DValue list_header;
list_header.set_array();
DValue list_item;
list_item = "not-an-object";
list_header.push(list_item);
DValue list_request = sign_request;
list_request["protected_header"] = list_header;
DValue control_request = sign_request;
control_request["claims"]["bad"] = String("control\nbyte");
DValue oversized = key_request;
oversized["ignored"] = String(17000, 'x');
check("crypto_operation() ES256 key generation and JWT signing", key["ok"].to_bool() && signed_result["ok"].to_bool() && key["private_jwk"]["kty"].to_string() == "EC" && key["private_jwk"]["crv"].to_string() == "P-256" && key["public_jwk"]["d"].to_string() == "" && key["kid"].to_string() == key["thumbprint"].to_string() && parts.size() == 3 && parts[2].size() == 86 && decoded_header.find("ES256") != String::npos && !crypto_operation(wrong_request)["ok"].to_bool() && !crypto_operation(malformed_request)["ok"].to_bool() && crypto_operation(unsupported)["error"].to_string() == "unsupported_operation" && !crypto_operation(list_request)["ok"].to_bool() && crypto_operation(control_request)["error"].to_string() == "invalid_request" && crypto_operation(oversized)["error"].to_string() == "invalid_request", key["kid"].to_string());
site_tests_summary(passed, failed, skipped, "Structured crypto tests generate ephemeral P-256 keys and retain no key material.");
site_tests_page_end();
}
+1
View File
@@ -8,6 +8,7 @@ markdown.uce|Markdown|Markdown parsing, rendering, and component hooks.|http sui
units.uce|Units|unit_call(), lifecycle hooks, and unit metadata.|http suite uce public|Units|1|1
websockets.ws.uce|WebSockets|Browser-driven WebSocket helper checks.|http suite uce public websocket|WebSockets|1|1
io.uce|Filesystem|Filesystem helpers that are restricted outside trusted networks.|http suite uce internal|Filesystem|1|1
crypto_operation.uce|Structured Crypto|Algorithm-selected key generation and JWT signing coverage.|http suite uce public crypto|Structured crypto operations|1|1
sqlite.uce|SQLite|SQLite connector with prepared named parameters and DValue rows.|http suite uce internal sqlite|SQLite|1|1
zip.uce|ZIP|Archive helpers that create and extract temporary server-side files.|http suite uce internal|ZIP|1|1
services.uce|Sockets And Services|Network/service helpers that are restricted outside trusted networks.|http suite uce internal|Sockets And Services|1|1
+303
View File
@@ -41,6 +41,7 @@ void SHA1Final(unsigned char digest[20], SHA1_CTX* context);
#include <string.h>
#include <sys/types.h> /* for u_int*_t */
#include "hash.h"
#include "uri.h"
#ifndef BYTE_ORDER
#if (BSD >= 199103)
@@ -422,8 +423,63 @@ bool crypto_equal_native(String a, String b)
return(diff == 0);
}
static bool uce_crypto_utf8_json_string(String value)
{
for(size_t i = 0; i < value.size();)
{
u8 c = (u8)value[i];
if(c < 0x20) return(false);
if(c < 0x80) { i++; continue; }
size_t need = c >= 0xC2 && c <= 0xDF ? 1 : (c >= 0xE0 && c <= 0xEF ? 2 : (c >= 0xF0 && c <= 0xF4 ? 3 : 0));
if(need == 0 || i + need >= value.size()) return(false);
for(size_t j = 1; j <= need; j++) if(((u8)value[i + j] & 0xC0) != 0x80) return(false);
u8 second = (u8)value[i + 1];
if((c == 0xE0 && second < 0xA0) || (c == 0xED && second >= 0xA0) || (c == 0xF0 && second < 0x90) || (c == 0xF4 && second >= 0x90)) return(false);
i += need + 1;
}
return(true);
}
static bool uce_crypto_value_valid(const DValue& value, size_t depth, size_t& nodes, size_t& bytes)
{
if(depth > 16 || ++nodes > 256) return(false);
const DValue& item = value.deref();
if(item.type == 'S')
{
bytes += item._String.size();
return(bytes <= 16384 && uce_crypto_utf8_json_string(item._String));
}
if(item.type == 'F') return(std::isfinite(item._float));
if(item.type == 'B') return(true);
if(item.type != 'M') return(false);
bool list = item.is_list();
for(const auto& child : item._map)
{
if(!list)
{
bytes += child.first.size();
if(bytes > 16384 || !uce_crypto_utf8_json_string(child.first)) return(false);
}
if(!uce_crypto_value_valid(child.second, depth + 1, nodes, bytes)) return(false);
}
return(true);
}
bool crypto_operation_request_valid(DValue request)
{
const DValue& root = request.deref();
if(root.type != 'M' || root.is_list()) return(false);
size_t nodes = 0, bytes = 0;
return(uce_crypto_value_valid(root, 0, nodes, bytes));
}
#ifndef __UCE_WASM_CORE__
#include <openssl/bn.h>
#include <openssl/core_names.h>
#include <openssl/ecdsa.h>
#include <openssl/evp.h>
#include <openssl/params.h>
#include <openssl/param_build.h>
#include <openssl/rand.h>
namespace {
@@ -525,4 +581,251 @@ bool password_needs_rehash_native(String encoded)
String salt, digest;
return(!uce_password_parts(encoded, n, r, p, salt, digest) || n != UCE_PASSWORD_SCRYPT_N || r != UCE_PASSWORD_SCRYPT_R || p != UCE_PASSWORD_SCRYPT_P);
}
namespace {
static constexpr size_t UCE_ES256_COORDINATE_BYTES = 32;
static constexpr size_t UCE_ES256_SIGNATURE_BYTES = 64;
static constexpr size_t UCE_ES256_JSON_MAX = 16 * 1024;
static constexpr size_t UCE_ES256_VALUE_MAX = 256;
static constexpr size_t UCE_ES256_DEPTH_MAX = 16;
struct UcePkeyDeleter { void operator()(EVP_PKEY* value) const { EVP_PKEY_free(value); } };
struct UcePkeyCtxDeleter { void operator()(EVP_PKEY_CTX* value) const { EVP_PKEY_CTX_free(value); } };
struct UceEcdsaSigDeleter { void operator()(ECDSA_SIG* value) const { ECDSA_SIG_free(value); } };
struct UceBnDeleter { void operator()(BIGNUM* value) const { BN_clear_free(value); } };
struct UceParamBldDeleter { void operator()(OSSL_PARAM_BLD* value) const { OSSL_PARAM_BLD_free(value); } };
struct UceParamsDeleter { void operator()(OSSL_PARAM* value) const { OSSL_PARAM_free(value); } };
static String uce_base64url_encode(const unsigned char* bytes, size_t size)
{
String encoded = base64_encode(String((const char*)bytes, size));
encoded = replace(replace(encoded, "+", "-"), "/", "_");
while(!encoded.empty() && encoded.back() == '=') encoded.pop_back();
return(encoded);
}
static bool uce_base64url_decode(String encoded, String& decoded)
{
if(encoded.empty() || encoded.size() > 128 || encoded.find('=') != String::npos || encoded.size() % 4 == 1)
return(false);
for(char c : encoded)
if(!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '-' && c != '_')
return(false);
String padded = replace(replace(encoded, "-", "+"), "_", "/");
while(padded.size() % 4) padded += "=";
bool ok = false;
decoded = base64_decode(padded, ok);
return(ok && uce_base64url_encode((const unsigned char*)decoded.data(), decoded.size()) == encoded);
}
static bool uce_es256_json_value(const DValue& value, size_t depth, size_t& values, size_t& bytes)
{
if(depth > UCE_ES256_DEPTH_MAX || ++values > UCE_ES256_VALUE_MAX)
return(false);
const DValue& item = value.deref();
if(item.type == 'S')
return((bytes += item._String.size()) <= UCE_ES256_JSON_MAX);
if(item.type == 'F' || item.type == 'B')
return(true);
if(item.type != 'M')
return(false);
for(const auto& child : item._map)
{
if((bytes += child.first.size()) > UCE_ES256_JSON_MAX || !uce_es256_json_value(child.second, depth + 1, values, bytes))
return(false);
}
return(true);
}
static bool uce_es256_json_map(const DValue& value)
{
if(value.deref().type != 'M' || value.deref().is_list())
return(false);
size_t values = 0, bytes = 0;
return(uce_es256_json_value(value, 0, values, bytes));
}
static bool uce_es256_jwk_string(const DValue& jwk, const String& field, String& value)
{
const DValue* found = jwk.key(field);
if(!found)
return(false);
const DValue& item = found->deref();
if(item.type != 'S' || item._String.empty() || item._String.size() > 128)
return(false);
value = item._String;
return(true);
}
static std::unique_ptr<EVP_PKEY, UcePkeyDeleter> uce_es256_key_from_jwk(const DValue& jwk)
{
if(jwk.deref().type != 'M')
return(nullptr);
String kty, crv, x64, y64, d64, x, y, d;
if(!uce_es256_jwk_string(jwk, "kty", kty) || !uce_es256_jwk_string(jwk, "crv", crv) || !uce_es256_jwk_string(jwk, "x", x64) || !uce_es256_jwk_string(jwk, "y", y64) || !uce_es256_jwk_string(jwk, "d", d64) ||
kty != "EC" || crv != "P-256" || !uce_base64url_decode(x64, x) || !uce_base64url_decode(y64, y) || !uce_base64url_decode(d64, d) ||
x.size() != UCE_ES256_COORDINATE_BYTES || y.size() != UCE_ES256_COORDINATE_BYTES || d.size() != UCE_ES256_COORDINATE_BYTES)
return(nullptr);
unsigned char public_key[65];
public_key[0] = 4;
memcpy(public_key + 1, x.data(), x.size());
memcpy(public_key + 33, y.data(), y.size());
std::unique_ptr<BIGNUM, UceBnDeleter> private_bn(BN_bin2bn((const unsigned char*)d.data(), d.size(), 0));
std::unique_ptr<OSSL_PARAM_BLD, UceParamBldDeleter> builder(OSSL_PARAM_BLD_new());
if(!private_bn || !builder || OSSL_PARAM_BLD_push_utf8_string(builder.get(), OSSL_PKEY_PARAM_GROUP_NAME, "prime256v1", 0) <= 0 ||
OSSL_PARAM_BLD_push_octet_string(builder.get(), OSSL_PKEY_PARAM_PUB_KEY, public_key, sizeof(public_key)) <= 0 ||
OSSL_PARAM_BLD_push_BN(builder.get(), OSSL_PKEY_PARAM_PRIV_KEY, private_bn.get()) <= 0)
return(nullptr);
std::unique_ptr<OSSL_PARAM, UceParamsDeleter> params(OSSL_PARAM_BLD_to_param(builder.get()));
std::unique_ptr<EVP_PKEY_CTX, UcePkeyCtxDeleter> ctx(EVP_PKEY_CTX_new_from_name(0, "EC", 0));
EVP_PKEY* raw = 0;
if(!params || !ctx || EVP_PKEY_fromdata_init(ctx.get()) <= 0 || EVP_PKEY_fromdata(ctx.get(), &raw, EVP_PKEY_KEYPAIR, params.get()) <= 0)
return(nullptr);
std::unique_ptr<EVP_PKEY, UcePkeyDeleter> key(raw);
std::unique_ptr<EVP_PKEY_CTX, UcePkeyCtxDeleter> check(EVP_PKEY_CTX_new(key.get(), 0));
return(check && EVP_PKEY_public_check(check.get()) > 0 && EVP_PKEY_private_check(check.get()) > 0 && EVP_PKEY_pairwise_check(check.get()) > 0 ? std::move(key) : nullptr);
}
static bool uce_es256_key_coordinates(EVP_PKEY* key, String& x, String& y, String& d)
{
unsigned char public_key[65]; size_t public_key_size = sizeof(public_key);
BIGNUM* private_bn = 0;
if(EVP_PKEY_get_octet_string_param(key, OSSL_PKEY_PARAM_PUB_KEY, public_key, sizeof(public_key), &public_key_size) <= 0 || public_key_size != sizeof(public_key) || public_key[0] != 4 ||
EVP_PKEY_get_bn_param(key, OSSL_PKEY_PARAM_PRIV_KEY, &private_bn) <= 0)
return(false);
std::unique_ptr<BIGNUM, UceBnDeleter> private_key(private_bn);
if(BN_bn2binpad(private_key.get(), (unsigned char*)d.data(), UCE_ES256_COORDINATE_BYTES) != UCE_ES256_COORDINATE_BYTES)
return(false);
x.assign((const char*)public_key + 1, UCE_ES256_COORDINATE_BYTES);
y.assign((const char*)public_key + 33, UCE_ES256_COORDINATE_BYTES);
return(true);
}
static DValue uce_es256_jwk(String x, String y, String d = "")
{
DValue jwk;
jwk["kty"] = "EC";
jwk["crv"] = "P-256";
jwk["x"] = uce_base64url_encode((const unsigned char*)x.data(), x.size());
jwk["y"] = uce_base64url_encode((const unsigned char*)y.data(), y.size());
if(d != "") jwk["d"] = uce_base64url_encode((const unsigned char*)d.data(), d.size());
return(jwk);
}
static String uce_es256_thumbprint(const DValue& public_jwk)
{
const DValue* x = public_jwk.key("x");
const DValue* y = public_jwk.key("y");
if(!x || !y)
return("");
String canonical = "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"" + x->to_string() + "\",\"y\":\"" + y->to_string() + "\"}";
String digest = sha256_native(canonical);
return(uce_base64url_encode((const unsigned char*)digest.data(), digest.size()));
}
}
static DValue uce_es256_key_create()
{
std::unique_ptr<EVP_PKEY_CTX, UcePkeyCtxDeleter> ctx(EVP_PKEY_CTX_new_from_name(0, "EC", 0));
EVP_PKEY* raw = 0;
OSSL_PARAM params[] = { OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, (char*)"prime256v1", 0), OSSL_PARAM_construct_end() };
if(!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0 || EVP_PKEY_CTX_set_params(ctx.get(), params) <= 0 || EVP_PKEY_generate(ctx.get(), &raw) <= 0)
return(DValue());
std::unique_ptr<EVP_PKEY, UcePkeyDeleter> key(raw);
String x(UCE_ES256_COORDINATE_BYTES, 0), y(UCE_ES256_COORDINATE_BYTES, 0), d(UCE_ES256_COORDINATE_BYTES, 0);
if(!uce_es256_key_coordinates(key.get(), x, y, d))
return(DValue());
DValue result;
result["public_jwk"] = uce_es256_jwk(x, y);
String kid = uce_es256_thumbprint(result["public_jwk"]);
result["public_jwk"]["kid"] = kid;
result["private_jwk"] = uce_es256_jwk(x, y, d);
result["private_jwk"]["kid"] = kid;
result["kid"] = kid;
result["thumbprint"] = kid;
return(result);
}
static String uce_es256_jwt(DValue private_jwk, DValue protected_header, DValue claims)
{
if(!uce_es256_json_map(protected_header) || !uce_es256_json_map(claims))
return("");
std::unique_ptr<EVP_PKEY, UcePkeyDeleter> key = uce_es256_key_from_jwk(private_jwk);
if(!key)
return("");
protected_header["alg"] = "ES256";
String header_json = json_encode(protected_header);
String claims_json = json_encode(claims);
if(header_json.size() > UCE_ES256_JSON_MAX || claims_json.size() > UCE_ES256_JSON_MAX)
return("");
String signing_input = uce_base64url_encode((const unsigned char*)header_json.data(), header_json.size()) + "." + uce_base64url_encode((const unsigned char*)claims_json.data(), claims_json.size());
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free);
size_t der_size = 0;
if(!ctx || EVP_DigestSignInit(ctx.get(), 0, EVP_sha256(), 0, key.get()) <= 0 || EVP_DigestSign(ctx.get(), 0, &der_size, (const unsigned char*)signing_input.data(), signing_input.size()) <= 0 || der_size == 0 || der_size > 256)
return("");
String der(der_size, 0);
if(EVP_DigestSign(ctx.get(), (unsigned char*)der.data(), &der_size, (const unsigned char*)signing_input.data(), signing_input.size()) <= 0)
return("");
const unsigned char* cursor = (const unsigned char*)der.data();
std::unique_ptr<ECDSA_SIG, UceEcdsaSigDeleter> signature(d2i_ECDSA_SIG(0, &cursor, der_size));
const BIGNUM *r = 0, *s = 0;
if(!signature || cursor != (const unsigned char*)der.data() + der_size)
return("");
ECDSA_SIG_get0(signature.get(), &r, &s);
String jose(UCE_ES256_SIGNATURE_BYTES, 0);
if(!r || !s || BN_bn2binpad(r, (unsigned char*)jose.data(), UCE_ES256_COORDINATE_BYTES) != UCE_ES256_COORDINATE_BYTES || BN_bn2binpad(s, (unsigned char*)jose.data() + UCE_ES256_COORDINATE_BYTES, UCE_ES256_COORDINATE_BYTES) != UCE_ES256_COORDINATE_BYTES)
return("");
return(signing_input + "." + uce_base64url_encode((const unsigned char*)jose.data(), jose.size()));
}
DValue crypto_operation_native(DValue request)
{
DValue result;
result["ok"].set_bool(false);
if(!crypto_operation_request_valid(request))
{
result["error"] = "invalid_request";
return(result);
}
String operation, algorithm;
if(!uce_es256_jwk_string(request, "operation", operation) || !uce_es256_jwk_string(request, "algorithm", algorithm))
{
result["error"] = "invalid_request";
return(result);
}
if(algorithm != "ES256")
{
result["error"] = "unsupported_algorithm";
return(result);
}
if(operation == "key_generate")
{
DValue key = uce_es256_key_create();
if(key["private_jwk"]["d"].to_string() == "")
{
result["error"] = "operation_failed";
return(result);
}
key["ok"].set_bool(true);
key["operation"] = operation;
key["algorithm"] = algorithm;
return(key);
}
if(operation == "jwt_sign")
{
String jwt = uce_es256_jwt(request["private_jwk"], request["protected_header"], request["claims"]);
if(jwt == "")
{
result["error"] = "invalid_key_or_payload";
return(result);
}
result["ok"].set_bool(true);
result["operation"] = operation;
result["algorithm"] = algorithm;
result["jwt"] = jwt;
return(result);
}
result["error"] = "unsupported_operation";
return(result);
}
#endif
+4
View File
@@ -1,5 +1,7 @@
#pragma once
struct DValue;
/* ================ sha1.h ================ */
/*
SHA-1 in C
@@ -27,6 +29,8 @@ bool crypto_equal_native(String a, String b);
String password_hash_native(String password);
bool password_verify_native(String password, String encoded);
bool password_needs_rehash_native(String encoded);
bool crypto_operation_request_valid(DValue request);
DValue crypto_operation_native(DValue request);
String sha256(String data);
String sha256_hex(String data);
String hmac_sha256(String key, String data);
+21
View File
@@ -64,6 +64,7 @@ int uce_host_crypto_equal(const char* a, size_t a_len, const char* b, size_t b_l
size_t uce_host_password_hash(const char* password, size_t password_len, char* out, size_t cap);
int uce_host_password_verify(const char* password, size_t password_len, const char* encoded, size_t encoded_len);
int uce_host_password_needs_rehash(const char* encoded, size_t encoded_len);
size_t uce_host_crypto_operation(const char* in, size_t in_len, char* out, size_t cap);
size_t uce_host_http_request(const char* in, size_t in_len, char* out, size_t cap);
uint64_t uce_host_http_request_async(const char* in, size_t in_len);
size_t uce_host_shell_exec_dv(const char* in, size_t in_len, char* out, size_t cap);
@@ -284,6 +285,25 @@ String password_hash(String password)
}
bool password_verify(String password, String encoded) { return(uce_host_password_verify(password.data(), password.size(), encoded.data(), encoded.size()) != 0); }
bool password_needs_rehash(String encoded) { return(uce_host_password_needs_rehash(encoded.data(), encoded.size()) != 0); }
DValue crypto_operation(DValue request)
{
if(!crypto_operation_request_valid(request))
{
DValue result;
result["ok"].set_bool(false);
result["error"] = "invalid_request";
return(result);
}
String encoded = ucb_encode(request);
size_t required = uce_host_crypto_operation(encoded.data(), encoded.size(), 0, 0);
if(required == 0 || required > 64 * 1024)
return(DValue());
String out(required, 0);
size_t got = uce_host_crypto_operation(encoded.data(), encoded.size(), &out[0], required);
DValue result;
String error;
return(got <= required && ucb_decode(String(out.data(), got), result, &error) ? result : DValue());
}
String base64_decode(String raw) { return(wasm_string_hostcall_1(uce_host_base64_decode, raw)); }
String random_bytes(u64 n)
{
@@ -753,6 +773,7 @@ bool crypto_equal(String a, String b) { return(crypto_equal_native(a, b)); }
String password_hash(String password) { return(password_hash_native(password)); }
bool password_verify(String password, String encoded) { return(password_verify_native(password, encoded)); }
bool password_needs_rehash(String encoded) { return(password_needs_rehash_native(encoded)); }
DValue crypto_operation(DValue request) { return(crypto_operation_native(request)); }
// Single definitions for the native split build (declared extern in sys.h).
pid_t parent_pid = 0;
+1
View File
@@ -51,6 +51,7 @@ bool crypto_equal(String a, String b);
String password_hash(String password);
bool password_verify(String password, String encoded);
bool password_needs_rehash(String encoded);
DValue crypto_operation(DValue request);
String shell_escape(String raw);
String basename(String fn);
String dirname(String fn);
+2
View File
@@ -461,6 +461,8 @@ extern "C" void uce_wasm_link_anchors()
(void*)(double (*)(double))&cos,
(void*)(double (*)(double))&sin,
(void*)(double (*)(double))&round,
// structured crypto dispatcher is hostcall-backed and otherwise unreferenced by core
(void*)&crypto_operation,
};
(void)libc_anchors;
}
+1
View File
@@ -13,6 +13,7 @@ uce_host_crypto_equal
uce_host_password_hash
uce_host_password_verify
uce_host_password_needs_rehash
uce_host_crypto_operation
uce_host_task_spawn
uce_host_task_pid
uce_host_task_kill
+22
View File
@@ -3674,6 +3674,28 @@ private:
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String password,encoded; self->hostcall_read(args[0].i32(), args[1].i32(), password); self->hostcall_read(args[2].i32(), args[3].i32(), encoded); bool valid=password_verify_native(password,encoded); results[0]=Val((int32_t)(valid?1:0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_password_needs_rehash")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); results[0]=Val((int32_t)(password_needs_rehash_native(encoded)?1:0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_crypto_operation")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded, out;
DValue request, response;
String error;
int32_t input_size = args[1].i32();
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String stage_key = "crypto_operation";
if(!self->hostcall_staged(stage_key, out))
{
if(input_size > 0 && input_size <= 64 * 1024 && self->hostcall_read(args[0].i32(), input_size, encoded) == "" && ucb_decode(encoded, request, &error))
response = crypto_operation_native(request);
else
response["error"] = "invalid_request";
out = ucb_encode(response);
if(buf == 0) self->hostcall_stage(stage_key, out);
}
if(buf && cap >= out.size()) self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_log")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
String text;