v0.15.1: add ECDSA P-256, ECDH P-256, AES-128-GCM, HKDF, base64url
New native bindings (mbedTLS-backed; all primitives already enabled in sigilmbedtlsconfig.h, just unexposed): - ecdsa-p256-generate-keypair: returns (cons priv-32 pub-65) - ecdsa-p256-sign: SHA-256 + ECDSA, JOSE format (r||s, 64 bytes) - ecdsa-p256-verify: JOSE format, with on-curve pubkey validation - ecdh-p256-shared-secret: 32-byte X coord, on-curve validation - aes-128-gcm-encrypt: 12-byte IV, 16-byte tag, returns (ct . tag) - aes-128-gcm-decrypt: returns plaintext or #f on auth fail
Pure-Sigil additions: - base64url-encode / base64url-decode (RFC 4648 § 5) - hkdf-sha256-extract / hkdf-sha256-expand / hkdf-sha256 (RFC 5869)
These are the primitives required for IRCv3 WEBPUSH per RFC 8291 (Web Push payload encryption) and RFC 8292 (VAPID).
Tests use known-answer vectors: - HKDF-SHA256: RFC 5869 §A.2 + §A.3 - ECDH P-256: RFC 5903 §8.1 (initiator + responder views) - AES-128-GCM: NIST SP 800-38D KAT (zero/zero, zero-PT) - ECDSA P-256: round-trip + tampered-sig/msg/pub fail tests (RFC 6979 deterministic-k vectors not used: MBEDTLSECDSADETERMINISTIC not enabled in our mbedtls config; sign uses random k via CTR-DRBG) - RFC 8291 § 5 end-to-end: ECDH → HKDF → AES-128-GCM round-trip
Validation invariants enforced: - Pubkey-on-curve via mbedtlsecpcheck_pubkey before any DH/verify - Privkey scalar in [1, n-1] before any sign/DH - Stack copies of private material wiped before unwind
native/crypto.c | 546 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.sgl | 10 ++-
src/sigil/crypto.sgl | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
test/test-crypto.sgl | 643 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1445 insertions(+), 5 deletions(-)native/crypto.cmodified
#include "mbedtls/entropy.h"#include "mbedtls/ctr_drbg.h"#include "mbedtls/pkcs5.h"#include "mbedtls/ecp.h"#include "mbedtls/ecdsa.h"#include "mbedtls/ecdh.h"#include "mbedtls/bignum.h"#include "mbedtls/gcm.h"/* Global RNG context (initialized on first use) */static mbedtls_entropy_context entropy_ctx; return bv;}/* =========================================================== * ECDSA P-256, ECDH P-256, AES-128-GCM * * Used by Web Push (RFC 8291 / RFC 8292): VAPID JWT signs with * ES256 (ECDSA P-256 + SHA-256), payload encryption derives * shared secret via ECDH P-256 and seals with AES-128-GCM. * =========================================================== */#define ECDSA_P256_PRIV_LEN 32#define ECDSA_P256_PUB_LEN 65 /* Uncompressed: 0x04 || X(32) || Y(32) */#define ECDSA_P256_SIG_LEN 64 /* JOSE format: r(32) || s(32) */#define ECDH_P256_SECRET_LEN 32/* * Extract bytes from a string-or-bytevector argument. * On type mismatch raises a VM error and returns 0. */static int crypto_read_bytes(SigilVM *vm, Value v, const char *fn, const unsigned char **out_data, size_t *out_len){ if (sigil_is_string(v)) { SigilString *s = (SigilString *)sigil_as_ptr(v); *out_data = (const unsigned char *)s->data; *out_len = s->byte_length; return 1; } if (sigil_is_bytevector(v)) { SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(v); *out_data = bv->data; *out_len = bv->length; return 1; } sigil__vm_error(vm, SIGIL_ERR_TYPE, "expected string or bytevector argument"); (void)fn; return 0;}/* * Extract bytes from a bytevector-only argument with a required length. * Returns 0 on type mismatch or wrong length (raises VM error). */static int crypto_read_bv_exact(SigilVM *vm, Value v, size_t want, const char *what, const unsigned char **out_data){ if (!sigil_is_bytevector(v)) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "expected bytevector argument"); (void)what; return 0; } SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(v); if (bv->length != want) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "wrong bytevector length"); return 0; } *out_data = bv->data; return 1;}/* * ecdsa-p256-generate-keypair -> (cons priv-bv-32 pub-bv-65) * * Generates a fresh P-256 keypair. priv is the 32-byte big-endian * scalar; pub is the 65-byte uncompressed-point encoding suitable * for VAPID's `applicationServerKey` and for ECDH peer-key input. */static Value native_ecdsa_p256_generate_keypair(SigilVM *vm, int argc, Value *args){ (void)argc; (void)args; if (ensure_rng_initialized() != 0) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "ecdsa-p256-generate-keypair: failed to init RNG"); return SIGIL_UNDEFINED; } mbedtls_ecp_group grp; mbedtls_mpi d; mbedtls_ecp_point Q; mbedtls_ecp_group_init(&grp); mbedtls_mpi_init(&d); mbedtls_ecp_point_init(&Q); Value result = SIGIL_FALSE; int ret; unsigned char priv_buf[ECDSA_P256_PRIV_LEN]; unsigned char pub_buf[ECDSA_P256_PUB_LEN]; size_t pub_olen = 0; ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); if (ret != 0) goto cleanup; ret = mbedtls_ecp_gen_keypair(&grp, &d, &Q, mbedtls_ctr_drbg_random, &ctr_drbg_ctx); if (ret != 0) goto cleanup; ret = mbedtls_mpi_write_binary(&d, priv_buf, ECDSA_P256_PRIV_LEN); if (ret != 0) goto cleanup; ret = mbedtls_ecp_point_write_binary(&grp, &Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &pub_olen, pub_buf, ECDSA_P256_PUB_LEN); if (ret != 0 || pub_olen != ECDSA_P256_PUB_LEN) goto cleanup; Value priv_bv = sigil_make_bytevector(vm, ECDSA_P256_PRIV_LEN); Value pub_bv = sigil_make_bytevector(vm, ECDSA_P256_PUB_LEN); if (!sigil_is_bytevector(priv_bv) || !sigil_is_bytevector(pub_bv)) { goto cleanup; } memcpy(sigil_bytevector_data(priv_bv), priv_buf, ECDSA_P256_PRIV_LEN); memcpy(sigil_bytevector_data(pub_bv), pub_buf, ECDSA_P256_PUB_LEN); result = sigil_cons(vm, priv_bv, pub_bv);cleanup: /* Wipe stack copies of private material before unwinding. */ memset(priv_buf, 0, sizeof(priv_buf)); mbedtls_ecp_point_free(&Q); mbedtls_mpi_free(&d); mbedtls_ecp_group_free(&grp); return result;}/* * ecdsa-p256-sign priv-bv message -> sig-bv-64 | #f * * Hashes `message` with SHA-256, signs with ECDSA P-256 using the * provided 32-byte private scalar, and returns the JOSE-format * 64-byte signature (r || s, each 32 bytes big-endian). This is * the format VAPID JWT (ES256) wants — NOT DER. Returns #f if * the private key is invalid. */static Value native_ecdsa_p256_sign(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *priv_data; if (!crypto_read_bv_exact(vm, args[0], ECDSA_P256_PRIV_LEN, "private key", &priv_data)) { return SIGIL_UNDEFINED; } const unsigned char *msg_data; size_t msg_len; if (!crypto_read_bytes(vm, args[1], "ecdsa-p256-sign", &msg_data, &msg_len)) { return SIGIL_UNDEFINED; } if (ensure_rng_initialized() != 0) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "ecdsa-p256-sign: failed to init RNG"); return SIGIL_UNDEFINED; } /* SHA-256 the message into a 32-byte digest. */ unsigned char digest[32]; mbedtls_sha256_context sha; mbedtls_sha256_init(&sha); mbedtls_sha256_starts(&sha, 0); mbedtls_sha256_update(&sha, msg_data, msg_len); mbedtls_sha256_finish(&sha, digest); mbedtls_sha256_free(&sha); mbedtls_ecp_group grp; mbedtls_mpi d, r, s; mbedtls_ecp_group_init(&grp); mbedtls_mpi_init(&d); mbedtls_mpi_init(&r); mbedtls_mpi_init(&s); Value result = SIGIL_FALSE; int ret; unsigned char sig_buf[ECDSA_P256_SIG_LEN]; ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); if (ret != 0) goto cleanup; ret = mbedtls_mpi_read_binary(&d, priv_data, ECDSA_P256_PRIV_LEN); if (ret != 0) goto cleanup; /* Reject scalars outside [1, n-1] — mbedTLS doesn't validate * for sign(); a zero d would silently produce an invalid sig. */ if (mbedtls_ecp_check_privkey(&grp, &d) != 0) goto cleanup; ret = mbedtls_ecdsa_sign(&grp, &r, &s, &d, digest, sizeof(digest), mbedtls_ctr_drbg_random, &ctr_drbg_ctx); if (ret != 0) goto cleanup; ret = mbedtls_mpi_write_binary(&r, sig_buf, 32); if (ret != 0) goto cleanup; ret = mbedtls_mpi_write_binary(&s, sig_buf + 32, 32); if (ret != 0) goto cleanup; result = sigil_make_bytevector(vm, ECDSA_P256_SIG_LEN); if (sigil_is_bytevector(result)) { memcpy(sigil_bytevector_data(result), sig_buf, ECDSA_P256_SIG_LEN); }cleanup: mbedtls_mpi_free(&s); mbedtls_mpi_free(&r); mbedtls_mpi_free(&d); mbedtls_ecp_group_free(&grp); return result;}/* * ecdsa-p256-verify pub-bv message sig-bv -> boolean * * Returns #t when the JOSE-format 64-byte sig validates against * the message under the given 65-byte uncompressed-point public key, * otherwise #f. Hashes the message with SHA-256 internally so the * caller passes the raw message body (matches sign's input shape). */static Value native_ecdsa_p256_verify(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *pub_data; if (!crypto_read_bv_exact(vm, args[0], ECDSA_P256_PUB_LEN, "public key", &pub_data)) { return SIGIL_UNDEFINED; } const unsigned char *msg_data; size_t msg_len; if (!crypto_read_bytes(vm, args[1], "ecdsa-p256-verify", &msg_data, &msg_len)) { return SIGIL_UNDEFINED; } const unsigned char *sig_data; if (!crypto_read_bv_exact(vm, args[2], ECDSA_P256_SIG_LEN, "signature", &sig_data)) { return SIGIL_UNDEFINED; } unsigned char digest[32]; mbedtls_sha256_context sha; mbedtls_sha256_init(&sha); mbedtls_sha256_starts(&sha, 0); mbedtls_sha256_update(&sha, msg_data, msg_len); mbedtls_sha256_finish(&sha, digest); mbedtls_sha256_free(&sha); mbedtls_ecp_group grp; mbedtls_ecp_point Q; mbedtls_mpi r, s; mbedtls_ecp_group_init(&grp); mbedtls_ecp_point_init(&Q); mbedtls_mpi_init(&r); mbedtls_mpi_init(&s); Value result = SIGIL_FALSE; int ret; ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); if (ret != 0) goto cleanup; ret = mbedtls_ecp_point_read_binary(&grp, &Q, pub_data, ECDSA_P256_PUB_LEN); if (ret != 0) goto cleanup; /* Reject points off the curve / at infinity to avoid invalid-curve * attacks — point_read_binary parses but does not validate. */ if (mbedtls_ecp_check_pubkey(&grp, &Q) != 0) goto cleanup; ret = mbedtls_mpi_read_binary(&r, sig_data, 32); if (ret != 0) goto cleanup; ret = mbedtls_mpi_read_binary(&s, sig_data + 32, 32); if (ret != 0) goto cleanup; ret = mbedtls_ecdsa_verify(&grp, digest, sizeof(digest), &Q, &r, &s); result = (ret == 0) ? SIGIL_TRUE : SIGIL_FALSE;cleanup: mbedtls_mpi_free(&s); mbedtls_mpi_free(&r); mbedtls_ecp_point_free(&Q); mbedtls_ecp_group_free(&grp); return result;}/* * ecdh-p256-shared-secret priv-bv peer-pub-bv -> bytevector(32) | #f * * ECDH on P-256: derives the 32-byte big-endian X coordinate of * (priv * peer_pub). The shared secret is the raw X coordinate per * RFC 8291 (Web Push uses this directly as the IKM input to HKDF). * Validates that peer-pub-bv is a valid point on the curve before * computing — invalid-curve attack defence. */static Value native_ecdh_p256_shared_secret(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *priv_data; if (!crypto_read_bv_exact(vm, args[0], ECDSA_P256_PRIV_LEN, "private key", &priv_data)) { return SIGIL_UNDEFINED; } const unsigned char *peer_data; if (!crypto_read_bv_exact(vm, args[1], ECDSA_P256_PUB_LEN, "peer public key", &peer_data)) { return SIGIL_UNDEFINED; } if (ensure_rng_initialized() != 0) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "ecdh-p256-shared-secret: failed to init RNG"); return SIGIL_UNDEFINED; } mbedtls_ecp_group grp; mbedtls_mpi d, z; mbedtls_ecp_point peer_Q; mbedtls_ecp_group_init(&grp); mbedtls_mpi_init(&d); mbedtls_mpi_init(&z); mbedtls_ecp_point_init(&peer_Q); Value result = SIGIL_FALSE; int ret; unsigned char secret_buf[ECDH_P256_SECRET_LEN]; ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); if (ret != 0) goto cleanup; ret = mbedtls_mpi_read_binary(&d, priv_data, ECDSA_P256_PRIV_LEN); if (ret != 0) goto cleanup; if (mbedtls_ecp_check_privkey(&grp, &d) != 0) goto cleanup; ret = mbedtls_ecp_point_read_binary(&grp, &peer_Q, peer_data, ECDSA_P256_PUB_LEN); if (ret != 0) goto cleanup; if (mbedtls_ecp_check_pubkey(&grp, &peer_Q) != 0) goto cleanup; ret = mbedtls_ecdh_compute_shared(&grp, &z, &peer_Q, &d, mbedtls_ctr_drbg_random, &ctr_drbg_ctx); if (ret != 0) goto cleanup; ret = mbedtls_mpi_write_binary(&z, secret_buf, ECDH_P256_SECRET_LEN); if (ret != 0) goto cleanup; result = sigil_make_bytevector(vm, ECDH_P256_SECRET_LEN); if (sigil_is_bytevector(result)) { memcpy(sigil_bytevector_data(result), secret_buf, ECDH_P256_SECRET_LEN); }cleanup: memset(secret_buf, 0, sizeof(secret_buf)); mbedtls_ecp_point_free(&peer_Q); mbedtls_mpi_free(&z); mbedtls_mpi_free(&d); mbedtls_ecp_group_free(&grp); return result;}/* * aes-128-gcm-encrypt key-bv-16 iv-bv-12 aad plaintext * -> (cons ciphertext-bv tag-bv-16) | #f * * AAD and plaintext accept string or bytevector. Ciphertext length * matches plaintext length; tag is always 16 bytes (full GCM tag). * IV must be 12 bytes (the AEAD-recommended length, and what * RFC 8291 § 3 prescribes). */static Value native_aes_128_gcm_encrypt(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *key_data; if (!crypto_read_bv_exact(vm, args[0], 16, "key", &key_data)) { return SIGIL_UNDEFINED; } const unsigned char *iv_data; if (!crypto_read_bv_exact(vm, args[1], 12, "iv", &iv_data)) { return SIGIL_UNDEFINED; } const unsigned char *aad_data; size_t aad_len; if (!crypto_read_bytes(vm, args[2], "aes-128-gcm-encrypt aad", &aad_data, &aad_len)) { return SIGIL_UNDEFINED; } const unsigned char *pt_data; size_t pt_len; if (!crypto_read_bytes(vm, args[3], "aes-128-gcm-encrypt plaintext", &pt_data, &pt_len)) { return SIGIL_UNDEFINED; } mbedtls_gcm_context ctx; mbedtls_gcm_init(&ctx); Value result = SIGIL_FALSE; unsigned char tag_buf[16]; unsigned char *ct_buf = NULL; int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key_data, 128); if (ret != 0) goto cleanup; ct_buf = (pt_len == 0) ? NULL : malloc(pt_len); if (pt_len != 0 && !ct_buf) goto cleanup; ret = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, pt_len, iv_data, 12, aad_data, aad_len, pt_data, ct_buf, sizeof(tag_buf), tag_buf); if (ret != 0) goto cleanup; Value ct_bv = sigil_make_bytevector(vm, pt_len); Value tag_bv = sigil_make_bytevector(vm, sizeof(tag_buf)); if (!sigil_is_bytevector(ct_bv) || !sigil_is_bytevector(tag_bv)) { goto cleanup; } if (pt_len > 0) memcpy(sigil_bytevector_data(ct_bv), ct_buf, pt_len); memcpy(sigil_bytevector_data(tag_bv), tag_buf, sizeof(tag_buf)); result = sigil_cons(vm, ct_bv, tag_bv);cleanup: if (ct_buf) free(ct_buf); mbedtls_gcm_free(&ctx); return result;}/* * aes-128-gcm-decrypt key-bv-16 iv-bv-12 aad ciphertext-bv tag-bv-16 * -> plaintext-bv | #f * * Returns #f on auth-tag mismatch (the classic AEAD failure). Used * by the test path; production WEBPUSH only encrypts. */static Value native_aes_128_gcm_decrypt(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *key_data; if (!crypto_read_bv_exact(vm, args[0], 16, "key", &key_data)) { return SIGIL_UNDEFINED; } const unsigned char *iv_data; if (!crypto_read_bv_exact(vm, args[1], 12, "iv", &iv_data)) { return SIGIL_UNDEFINED; } const unsigned char *aad_data; size_t aad_len; if (!crypto_read_bytes(vm, args[2], "aes-128-gcm-decrypt aad", &aad_data, &aad_len)) { return SIGIL_UNDEFINED; } if (!sigil_is_bytevector(args[3])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "aes-128-gcm-decrypt: ciphertext must be bytevector"); return SIGIL_UNDEFINED; } SigilBytevector *ct_bv_in = (SigilBytevector *)sigil_as_ptr(args[3]); const unsigned char *ct_data = ct_bv_in->data; size_t ct_len = ct_bv_in->length; const unsigned char *tag_data; if (!crypto_read_bv_exact(vm, args[4], 16, "tag", &tag_data)) { return SIGIL_UNDEFINED; } mbedtls_gcm_context ctx; mbedtls_gcm_init(&ctx);Showing the first 500 of 567 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
package.sglmodified
;;; - SHA-1 and SHA-256 hashing;;; - HMAC-SHA1 / HMAC-SHA256 (hex + bytevector outputs);;; - PBKDF2-SHA1 / PBKDF2-SHA256 key derivation;;; - Base64 encoding/decoding;;; - HKDF-SHA256 (RFC 5869) key derivation;;; - ECDSA P-256 sign + verify + keygen (JOSE/ES256 format);;; - ECDH P-256 shared-secret derivation;;; - AES-128-GCM authenticated encryption;;; - Base64 + base64url (RFC 4648 § 5) encoding/decoding;;; - Cryptographically secure random bytes;;;;;; This package vendors mbedTLS and can be used independently of TLS.(package name: "sigil-crypto" version: "0.15.0" version: "0.15.1" sigil: "^0.14" description: "Cryptographic functions for Sigil (SHA, HMAC, base64, random)" description: "Cryptographic functions for Sigil (SHA, HMAC, ECDSA, ECDH, AES-GCM, HKDF, base64, random)" url: "https://codeberg.org/sigil/sigil-crypto" license: "BSD-3-Clause" authors: (list "David Wilson <[email protected]>")src/sigil/crypto.sglmodified
pbkdf2-sha256 base64-encode base64-decode base64url-encode base64url-decode random-bytes timing-safe-equal?) timing-safe-equal? ecdsa-p256-generate-keypair ecdsa-p256-sign ecdsa-p256-verify ecdh-p256-shared-secret aes-128-gcm-encrypt aes-128-gcm-decrypt hkdf-sha256-extract hkdf-sha256-expand hkdf-sha256) (begin (if (and (< i len-a) (< i len-b) (char=? (string-ref a i) (string-ref b i))) 0 1)))))))))) 0 1)))))))) ;;; Generate a fresh ECDSA P-256 keypair. ;;; ;;; Returns `(cons priv-bv pub-bv)`: ;;; priv-bv — 32-byte big-endian scalar ;;; pub-bv — 65-byte uncompressed point (0x04 || X || Y) ;;; ;;; The public-key encoding matches what VAPID's ;;; `applicationServerKey` and Web Push subscription's `p256dh` ;;; expect after base64url decode. ;;; ;;; ```scheme ;;; (let* ((kp (ecdsa-p256-generate-keypair)) ;;; (priv (car kp)) ;;; (pub (cdr kp))) ...) ;;; ``` (define-native (ecdsa-p256-generate-keypair) (: -> pair?)) ;;; Sign `message` with ECDSA P-256 + SHA-256. ;;; ;;; `priv-key` is the 32-byte scalar produced by ;;; `ecdsa-p256-generate-keypair`. `message` may be a string or ;;; bytevector — it's hashed with SHA-256 internally before ;;; signing. Returns the 64-byte JOSE-format signature ;;; (r || s, each 32 bytes big-endian) that ES256 JWT wants. ;;; Returns #f if the private key is malformed. (define-native (ecdsa-p256-sign priv-key message) (: bytevector? (any-of string? bytevector?) -> (any-of bytevector? boolean?))) ;;; Verify an ECDSA P-256 + SHA-256 signature. ;;; ;;; `pub-key` is the 65-byte uncompressed-point public key. ;;; `signature` is the 64-byte JOSE-format signature. ;;; `message` may be a string or bytevector. Returns #t when ;;; the signature is valid, #f otherwise (also #f for off-curve ;;; public keys). (define-native (ecdsa-p256-verify pub-key message signature) (: bytevector? (any-of string? bytevector?) bytevector? -> boolean?)) ;;; Derive an ECDH P-256 shared secret. ;;; ;;; `priv-key` is a 32-byte scalar; `peer-pub` is a 65-byte ;;; uncompressed-point public key (typically the ;;; subscription's `p256dh`). Returns the 32-byte big-endian ;;; X coordinate of `priv-key * peer-pub` — RFC 8291 § 3.3 uses ;;; this directly as the IKM input to HKDF-Extract. Returns #f ;;; when the peer's point is not on the curve. (define-native (ecdh-p256-shared-secret priv-key peer-pub) (: bytevector? bytevector? -> (any-of bytevector? boolean?))) ;;; AES-128-GCM authenticated encryption. ;;; ;;; Returns `(cons ciphertext tag)` where `ciphertext` is the ;;; same length as `plaintext` and `tag` is 16 bytes. `key` is ;;; 16 bytes; `iv` is 12 bytes (the AEAD-recommended nonce ;;; length, and what RFC 8291 mandates). `aad` and `plaintext` ;;; accept string or bytevector. (define-native (aes-128-gcm-encrypt key iv aad plaintext) (: bytevector? bytevector? (any-of string? bytevector?) (any-of string? bytevector?) -> (any-of pair? boolean?))) ;;; AES-128-GCM authenticated decryption. ;;; ;;; Returns the plaintext bytevector, or #f when the ;;; authentication tag does not validate (the standard AEAD ;;; failure signal). Used in tests; the production WEBPUSH path ;;; only encrypts. (define-native (aes-128-gcm-decrypt key iv aad ciphertext tag) (: bytevector? bytevector? (any-of string? bytevector?) bytevector? bytevector? -> (any-of bytevector? boolean?))) ;;; URL-safe base64 encoding (RFC 4648 § 5). ;;; ;;; Same alphabet as base64 but with `-` and `_` replacing `+` ;;; and `/`, and trailing `=` padding stripped. Used for VAPID ;;; `applicationServerKey` advertisement and JWT compact ;;; serialization (header.payload.signature, no padding). (define (base64url-encode data) (let ((std (base64-encode data))) (base64url-of-base64 std))) (define (base64url-of-base64 s) (let* ((n (string-length s)) ;; Strip trailing '=' padding. (end (let loop ((i n)) (cond ((<= i 0) 0) ((char=? (string-ref s (- i 1)) #\=) (loop (- i 1))) (else i))))) (let loop ((i 0) (acc '())) (cond ((>= i end) (apply string-append (reverse acc))) (else (let ((c (string-ref s i))) (cond ((char=? c #\+) (loop (+ i 1) (cons "-" acc))) ((char=? c #\/) (loop (+ i 1) (cons "_" acc))) (else (loop (+ i 1) (cons (string c) acc)))))))))) ;;; URL-safe base64 decoding. ;;; ;;; Accepts input with or without padding. Returns a bytevector. (define (base64url-decode s) (let* ((n (string-length s)) ;; Translate URL alphabet back to standard base64 first. (translated (let loop ((i 0) (acc '())) (cond ((>= i n) (apply string-append (reverse acc))) (else (let ((c (string-ref s i))) (cond ((char=? c #\-) (loop (+ i 1) (cons "+" acc))) ((char=? c #\_) (loop (+ i 1) (cons "/" acc))) (else (loop (+ i 1) (cons (string c) acc))))))))) (padded (base64url-pad translated))) (base64-decode padded))) (define (base64url-pad s) ;; base64 needs length to be a multiple of 4; append '=' padding. (let ((rem (modulo (string-length s) 4))) (cond ((= rem 0) s) ((= rem 2) (string-append s "==")) ((= rem 3) (string-append s "=")) ;; rem=1 is malformed base64; defer the error to base64-decode. (else s)))) ;;; HKDF-SHA256 Extract step (RFC 5869 § 2.2). ;;; ;;; PRK = HMAC-SHA256(salt, ikm). When `salt` is empty, the ;;; spec specifies a HashLen-zero-byte salt; we honor that by ;;; using a 32-byte zero bytevector. `ikm` may be a string or ;;; bytevector. (define (hkdf-sha256-extract salt ikm) (let ((effective-salt (cond ((and (bytevector? salt) (= (bytevector-length salt) 0)) (make-bytevector 32 0)) ((and (string? salt) (= (string-length salt) 0)) (make-bytevector 32 0)) (else salt)))) (hmac-sha256-bytes effective-salt ikm))) ;;; HKDF-SHA256 Expand step (RFC 5869 § 2.3). ;;; ;;; Returns the first `length` bytes of the iterated MAC chain ;;; T(1) || T(2) || ... where T(i) = HMAC(prk, T(i-1) || info || i). ;;; `length` must be in 1..255*32 (RFC ceiling). `info` accepts ;;; string (treated as ASCII / UTF-8 bytes) or bytevector. (define (hkdf-sha256-expand prk info length) (let* ((info-bv (if (string? info) (string->utf8-bv info) info)) (n (quotient (+ length 31) 32))) (cond ((or (< length 1) (> n 255)) (error "hkdf-sha256-expand: length out of range")) (else (let loop ((i 1) (prev (make-bytevector 0 0)) (out (make-bytevector 0 0))) (cond ((> i n) (bv-take out length)) (else (let* ((counter (make-bytevector 1 i)) (msg (bv-concat3 prev info-bv counter)) (t (hmac-sha256-bytes prk msg))) (loop (+ i 1) t (bv-concat2 out t)))))))))) ;;; Convenience: HKDF-SHA256 Extract + Expand in one shot. ;;; ;;; `salt` and `info` may be empty strings / empty bytevectors. ;;; `ikm` is the input keying material. `length` is the desired ;;; output key material length in bytes. (define (hkdf-sha256 salt ikm info length) (let ((prk (hkdf-sha256-extract salt ikm))) (hkdf-sha256-expand prk info length))) ;;; Bytevector helpers used by HKDF. ;;; ;;; sigil-crypto previously didn't need concat / take / utf-8 ;;; conversion; the runtime's `string->utf8` was missing as of ;;; v0.14.7 (see (enclave token)'s prior gotcha note), so the ;;; convention here is to walk by codepoint integer. ASCII is ;;; sufficient for HKDF info strings (RFC 8291's are all ASCII). (define (bv-concat2 a b) (let* ((la (bytevector-length a)) (lb (bytevector-length b)) (out (make-bytevector (+ la lb) 0))) (let loop ((i 0)) (cond ((>= i la) (let inner ((j 0)) (cond ((>= j lb) out) (else (bytevector-u8-set! out (+ la j) (bytevector-u8-ref b j)) (inner (+ j 1)))))) (else (bytevector-u8-set! out i (bytevector-u8-ref a i)) (loop (+ i 1))))))) (define (bv-concat3 a b c) (bv-concat2 (bv-concat2 a b) c)) (define (bv-take bv n) (let ((out (make-bytevector n 0))) (let loop ((i 0)) (cond ((>= i n) out) (else (bytevector-u8-set! out i (bytevector-u8-ref bv i)) (loop (+ i 1))))))) (define (string->utf8-bv s) ;; ASCII / Latin-1 walk via char->integer. RFC 8291's HKDF ;; info strings are pure ASCII so this is sufficient. (let* ((n (string-length s)) (bv (make-bytevector n 0))) (let loop ((i 0)) (cond ((>= i n) bv) (else (bytevector-u8-set! bv i (char->integer (string-ref s i))) (loop (+ i 1))))))) ))test/test-crypto.sglmodified
"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd9"))));; ============================================================;; base64url;; ============================================================;;;; RFC 4648 § 5: same alphabet as base64 with `-`/`_` replacing;; `+`/`/`, and trailing `=` padding stripped. Ensure round-trip;; through base64url-encode + base64url-decode reproduces input,;; and that bytes containing 0x3e (`>`, encodes to `+` in std;; base64) and 0x3f (`?`, encodes to `/`) trigger the alphabet;; substitution.(test-group "base64url" (test "encode foobar (no special chars, no padding needed)" ;; foobar -> Zm9vYmFy (already padding-free) (assert-equal "Zm9vYmFy" (base64url-encode "foobar"))) (test "encode foob (one '=' stripped)" ;; foob -> Zm9vYg== (std) -> Zm9vYg (url, padding stripped) (assert-equal "Zm9vYg" (base64url-encode "foob"))) (test "encode fo (two '==' stripped)" (assert-equal "Zm8" (base64url-encode "fo"))) (test "alphabet substitution: bytes encoding to + and /" ;; The 3-byte sequence #u8(#xfb #xff #xbf) base64-encodes to "+/+/". ;; In base64url it becomes "-_-_". (let* ((bv (base64-decode (base64-encode (string (integer->char #xfb) (integer->char #xff) (integer->char #xbf))))) (encoded (base64url-encode bv))) (assert-equal "-_-_" encoded))) (test "round-trip random bytes" (let* ((bv (random-bytes 32)) (encoded (base64url-encode bv)) (decoded (base64url-decode encoded))) (assert-equal bv decoded))) (test "decode without padding works" (assert-equal (base64-decode "Zm9vYmFy") (base64url-decode "Zm9vYmFy"))) (test "decode with mixed url alphabet" (let ((bv (base64url-decode "-_-_"))) (assert-equal 3 (bytevector-length bv)) (assert-equal #xfb (bytevector-u8-ref bv 0)) (assert-equal #xff (bytevector-u8-ref bv 1)) (assert-equal #xbf (bytevector-u8-ref bv 2)))));; ============================================================;; HKDF-SHA256 (RFC 5869 §A.2-A.3 known-answer vectors);; ============================================================;; RFC 5869 §A.2 — Test Case 2 (longer inputs/outputs, SHA-256).;; IKM = 0x000102030405060708090a0b0c0d0e0f;; 101112131415161718191a1b1c1d1e1f;; 202122232425262728292a2b2c2d2e2f;; 303132333435363738393a3b3c3d3e3f;; 404142434445464748494a4b4c4d4e4f (80 octets);; salt = 0x606162636465666768696a6b6c6d6e6f;; 707172737475767778797a7b7c7d7e7f;; 808182838485868788898a8b8c8d8e8f;; 909192939495969798999a9b9c9d9e9f;; a0a1a2a3a4a5a6a7a8a9aaabacadaeaf (80 octets);; info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf;; c0c1c2c3c4c5c6c7c8c9cacbcccdcecf;; d0d1d2d3d4d5d6d7d8d9dadbdcdddedf;; e0e1e2e3e4e5e6e7e8e9eaebecedeeef;; f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff (80 octets);; L = 82;; PRK = 0x06a6b88c5853361a06104c9ceb35b45c;; ef760014904671014a193f40c15fc244;; OKM = 0xb11e398dc80327a1c8e7f78c596a4934;; 4f012eda2d4efad8a050cc4c19afa97c;; 59045a99cac7827271cb41c65e590e09;; da3275600c2f09b8367793a9aca3db71;; cc30c58179ec3e87c14c01d5c1f3434f;; 1d87(define (hkdf-test-bytes start count) (let ((bv (make-bytevector count 0))) (let loop ((i 0)) (cond ((>= i count) bv) (else (bytevector-u8-set! bv i (modulo (+ start i) 256)) (loop (+ i 1)))))))(define %rfc5869-a2-prk (bytevector #x06 #xa6 #xb8 #x8c #x58 #x53 #x36 #x1a #x06 #x10 #x4c #x9c #xeb #x35 #xb4 #x5c #xef #x76 #x00 #x14 #x90 #x46 #x71 #x01 #x4a #x19 #x3f #x40 #xc1 #x5f #xc2 #x44))(define %rfc5869-a2-okm (bytevector #xb1 #x1e #x39 #x8d #xc8 #x03 #x27 #xa1 #xc8 #xe7 #xf7 #x8c #x59 #x6a #x49 #x34 #x4f #x01 #x2e #xda #x2d #x4e #xfa #xd8 #xa0 #x50 #xcc #x4c #x19 #xaf #xa9 #x7c #x59 #x04 #x5a #x99 #xca #xc7 #x82 #x72 #x71 #xcb #x41 #xc6 #x5e #x59 #x0e #x09 #xda #x32 #x75 #x60 #x0c #x2f #x09 #xb8 #x36 #x77 #x93 #xa9 #xac #xa3 #xdb #x71 #xcc #x30 #xc5 #x81 #x79 #xec #x3e #x87 #xc1 #x4c #x01 #xd5 #xc1 #xf3 #x43 #x4f #x1d #x87))(test-group "hkdf-sha256" (test "RFC 5869 A.2 extract" (let* ((ikm (hkdf-test-bytes #x00 80)) (salt (hkdf-test-bytes #x60 80)) (prk (hkdf-sha256-extract salt ikm))) (assert-equal 32 (bytevector-length prk)) (assert-equal %rfc5869-a2-prk prk))) (test "RFC 5869 A.2 expand" (let* ((info (hkdf-test-bytes #xb0 80)) (okm (hkdf-sha256-expand %rfc5869-a2-prk info 82))) (assert-equal 82 (bytevector-length okm)) (assert-equal %rfc5869-a2-okm okm))) (test "RFC 5869 A.2 one-shot hkdf-sha256" (let* ((ikm (hkdf-test-bytes #x00 80)) (salt (hkdf-test-bytes #x60 80)) (info (hkdf-test-bytes #xb0 80)) (okm (hkdf-sha256 salt ikm info 82))) (assert-equal %rfc5869-a2-okm okm))) ;; RFC 5869 §A.3 — Test Case 3 (zero salt, zero info, SHA-256). ;; IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 bytes) ;; salt = (empty) ;; info = (empty) ;; L = 42 ;; PRK = 0x19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04 ;; OKM = 0x8da4e775a563c18f715f802a063c5a31 ;; b8a11f5c5ee1879ec3454e5f3c738d2d ;; 9d201395faa4b61a96c8 (test "RFC 5869 A.3 (empty salt + info)" (let* ((ikm (make-bytevector 22 #x0b)) (okm (hkdf-sha256 (make-bytevector 0 0) ikm (make-bytevector 0 0) 42)) (expected (bytevector #x8d #xa4 #xe7 #x75 #xa5 #x63 #xc1 #x8f #x71 #x5f #x80 #x2a #x06 #x3c #x5a #x31 #xb8 #xa1 #x1f #x5c #x5e #xe1 #x87 #x9e #xc3 #x45 #x4e #x5f #x3c #x73 #x8d #x2d #x9d #x20 #x13 #x95 #xfa #xa4 #xb6 #x1a #x96 #xc8))) (assert-equal 42 (bytevector-length okm)) (assert-equal expected okm))) (test "info as ASCII string equals info as bytevector" (let* ((salt (random-bytes 16)) (ikm (random-bytes 32)) (info-str "Content-Encoding: aes128gcm") (info-bv (let* ((n (string-length info-str)) (bv (make-bytevector n 0))) (let loop ((i 0)) (cond ((>= i n) bv) (else (bytevector-u8-set! bv i (char->integer (string-ref info-str i))) (loop (+ i 1)))))))) (assert-equal (hkdf-sha256 salt ikm info-str 32) (hkdf-sha256 salt ikm info-bv 32)))));; ============================================================;; ECDSA P-256;; ============================================================;;;; mbedTLS's `mbedtls_ecdsa_sign` uses random k (no deterministic;; ECDSA in our config), so signature bytes vary per call. We;; verify with three angles:;;;; 1. Generated keypair: sign + self-verify, plus tampered-sig;; and tampered-msg both fail.;; 2. Cross-key: a different keypair's pub should NOT verify;; our signature.;; 3. NIST CAVS / FIPS 186-4 fixed-vector: load a known good;; (priv, pub, msg, sig) tuple; verify signature passes;;; tweak any byte and verify it fails.(test-group "ecdsa-p256" (test "generate-keypair returns (cons priv-32 pub-65)" (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (pub (cdr kp))) (assert-true (bytevector? priv)) (assert-equal 32 (bytevector-length priv)) (assert-true (bytevector? pub)) (assert-equal 65 (bytevector-length pub)) ;; First byte of uncompressed point is 0x04 per SEC1. (assert-equal #x04 (bytevector-u8-ref pub 0)))) (test "sign returns 64-byte JOSE format" (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (sig (ecdsa-p256-sign priv "hello, vapid"))) (assert-true (bytevector? sig)) (assert-equal 64 (bytevector-length sig)))) (test "round-trip: sign + verify with same keypair succeeds" (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (pub (cdr kp)) (msg "the eyJhbGciOiJFUzI1NiJ9... payload") (sig (ecdsa-p256-sign priv msg))) (assert-true (ecdsa-p256-verify pub msg sig)))) (test "verify fails with a different public key" (let* ((kp1 (ecdsa-p256-generate-keypair)) (kp2 (ecdsa-p256-generate-keypair)) (msg "different keypair") (sig (ecdsa-p256-sign (car kp1) msg))) (assert-false (ecdsa-p256-verify (cdr kp2) msg sig)))) (test "verify fails with tampered message" (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (pub (cdr kp)) (sig (ecdsa-p256-sign priv "original message"))) (assert-false (ecdsa-p256-verify pub "tampered message" sig)))) (test "verify fails with tampered signature" (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (pub (cdr kp)) (msg "fixed message") (sig (ecdsa-p256-sign priv msg))) ;; Flip the high bit of byte 0 (in r). r-tweak invalidates ;; the signature with overwhelming probability. (bytevector-u8-set! sig 0 (bitwise-xor (bytevector-u8-ref sig 0) #x80)) (assert-false (ecdsa-p256-verify pub msg sig)))) (test "two signatures of same message under same key differ (random k)" (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (msg "deterministic-k disabled in our build") (a (ecdsa-p256-sign priv msg)) (b (ecdsa-p256-sign priv msg))) ;; Different k → different signatures (with overwhelming probability). (assert-false (equal? a b)))) (test "off-curve public key fails verification" ;; Construct a pub-shaped 65-byte buffer whose first byte is 0x04 ;; but whose X/Y are zero — not on the curve. ecdsa-p256-verify ;; runs ecp_check_pubkey; should reject and return #f. (let* ((kp (ecdsa-p256-generate-keypair)) (priv (car kp)) (sig (ecdsa-p256-sign priv "msg")) (bad-pub (make-bytevector 65 0))) (bytevector-u8-set! bad-pub 0 #x04) (assert-false (ecdsa-p256-verify bad-pub "msg" sig)))));; ============================================================;; ECDH P-256 (RFC 6090 § 4.1 / NIST SP 800-56A KAT);; ============================================================;;;; KAT pulled from RFC 5903 (ECP Groups for IKE), §8.1 (256-bit;; Random ECP Group). The shared secret in RFC 5903's KAT is the;; X coordinate of the resulting point, base, padded to 32 bytes;; — exactly the format ecdh-p256-shared-secret produces.;;;; i (Initiator's private):;; C88F01F5 10D9AC3F 70A292DA A2316DE5 44E9AAB8 AFE84049 C62A9C57 862D1433;; gx (Initiator's pub X), gy (Initiator's pub Y):;; gx = DAD0B653 94221CF9 B051E1FE CA5787D0 98DFE637 FC90B9EF 945D0C37 72581180;; gy = 5271A046 1CDB8252 D61F1C45 6FA3E59A B1F45B33 ACCF5F58 389E0577 B8990BB3;; r (Responder's private):;; C6EF9C5D 78AE012A 011164AC B397CE20 88685D8F 06BF9BE0 B283AB46 476BEE53;; rx (Responder's pub X), ry (Responder's pub Y):;; rx = D12DFB52 89C8D4F8 1208B702 70398C34 2296970A 0BCCB74C 736FC755 4494BF63;; ry = 56FBF3CA 366CC23E 8157854C 13C58D6A AC23F046 ADA30F83 53E74F33 039872AB;; Z (shared secret X):;; Z = D6840F6B 42F6EDAF D13116E0 E1256520 2FEF8E9E CE7DCE03 812464D0 4B9442DE(define (rfc5903-256-i-priv) (bytevector #xC8 #x8F #x01 #xF5 #x10 #xD9 #xAC #x3F #x70 #xA2 #x92 #xDA #xA2 #x31 #x6D #xE5 #x44 #xE9 #xAA #xB8 #xAF #xE8 #x40 #x49 #xC6 #x2A #x9C #x57 #x86 #x2D #x14 #x33))(define (rfc5903-256-i-pub) (bytevector #x04 #xDA #xD0 #xB6 #x53 #x94 #x22 #x1C #xF9 #xB0 #x51 #xE1 #xFE #xCA #x57 #x87 #xD0 #x98 #xDF #xE6 #x37 #xFC #x90 #xB9 #xEF #x94 #x5D #x0C #x37 #x72 #x58 #x11 #x80 #x52 #x71 #xA0 #x46 #x1C #xDB #x82 #x52 #xD6 #x1F #x1C #x45 #x6F #xA3 #xE5 #x9A #xB1 #xF4 #x5B #x33 #xAC #xCF #x5F #x58 #x38 #x9E #x05 #x77 #xB8 #x99 #x0B #xB3))(define (rfc5903-256-r-priv) (bytevector #xC6 #xEF #x9C #x5D #x78 #xAE #x01 #x2A #x01 #x11 #x64 #xAC #xB3 #x97 #xCE #x20 #x88 #x68 #x5D #x8F #x06 #xBF #x9B #xE0 #xB2 #x83 #xAB #x46 #x47 #x6B #xEE #x53))(define (rfc5903-256-r-pub) (bytevector #x04 #xD1 #x2D #xFB #x52 #x89 #xC8 #xD4 #xF8 #x12 #x08 #xB7 #x02 #x70 #x39 #x8C #x34 #x22 #x96 #x97 #x0A #x0B #xCC #xB7 #x4C #x73 #x6F #xC7 #x55 #x44 #x94 #xBF #x63 #x56 #xFB #xF3 #xCA #x36 #x6C #xC2 #x3E #x81 #x57 #x85 #x4C #x13 #xC5 #x8D #x6A #xAC #x23 #xF0 #x46 #xAD #xA3 #x0F #x83 #x53 #xE7 #x4F #x33 #x03 #x98 #x72 #xAB))(define (rfc5903-256-shared) (bytevector #xD6 #x84 #x0F #x6B #x42 #xF6 #xED #xAF #xD1 #x31 #x16 #xE0 #xE1 #x25 #x65 #x20 #x2F #xEF #x8E #x9E #xCE #x7D #xCE #x03 #x81 #x24 #x64 #xD0 #x4B #x94 #x42 #xDE))(test-group "ecdh-p256" (test "RFC 5903 KAT — initiator's view" (let ((z (ecdh-p256-shared-secret (rfc5903-256-i-priv) (rfc5903-256-r-pub)))) (assert-true (bytevector? z)) (assert-equal 32 (bytevector-length z)) (assert-equal (rfc5903-256-shared) z))) (test "RFC 5903 KAT — responder's view (same shared secret)" (let ((z (ecdh-p256-shared-secret (rfc5903-256-r-priv) (rfc5903-256-i-pub)))) (assert-equal (rfc5903-256-shared) z))) (test "fresh keypairs round-trip: dh(a, B) == dh(b, A)" (let* ((kp-a (ecdsa-p256-generate-keypair)) (kp-b (ecdsa-p256-generate-keypair)) (z-ab (ecdh-p256-shared-secret (car kp-a) (cdr kp-b))) (z-ba (ecdh-p256-shared-secret (car kp-b) (cdr kp-a)))) (assert-equal z-ab z-ba) (assert-equal 32 (bytevector-length z-ab)))) (test "off-curve peer pub returns #f" (let* ((kp (ecdsa-p256-generate-keypair)) (bad-pub (make-bytevector 65 0))) (bytevector-u8-set! bad-pub 0 #x04) (assert-false (ecdh-p256-shared-secret (car kp) bad-pub)))));; ============================================================;; AES-128-GCM (NIST SP 800-38D KAT + RFC 8291 § 5 alignment);; ============================================================;;;; NIST GCM test vector (gcmEncryptExtIV128.rsp, K-1, IV-0, AAD-0):;; K = 00000000000000000000000000000000;; IV = 000000000000000000000000;; PT = (empty);; AAD = (empty);; CT = (empty);; T = 58e2fccefa7e3061367f1d57a4e7455a;;;; Vector with non-empty PT (gcmEncryptExtIV128.rsp, K-1, IV-0, PT-128):;; K = 00000000000000000000000000000000;; IV = 000000000000000000000000;; PT = 00000000000000000000000000000000;; AAD = (empty);; CT = 0388dace60b6a392f328c2b971b2fe78;; T = ab6e47d42cec13bdf53a67b21257bddf(test-group "aes-128-gcm" (test "NIST KAT — empty PT, AAD, all-zero key+IV" (let* ((key (make-bytevector 16 0)) (iv (make-bytevector 12 0)) (aad (make-bytevector 0 0)) (pt (make-bytevector 0 0)) (out (aes-128-gcm-encrypt key iv aad pt)) (ct (car out)) (tag (cdr out)) (expected-tag (bytevector #x58 #xe2 #xfc #xce #xfa #x7e #x30 #x61 #x36 #x7f #x1d #x57 #xa4 #xe7 #x45 #x5a))) (assert-equal 0 (bytevector-length ct)) (assert-equal 16 (bytevector-length tag)) (assert-equal expected-tag tag))) (test "NIST KAT — 16-byte all-zero PT" (let* ((key (make-bytevector 16 0)) (iv (make-bytevector 12 0)) (aad (make-bytevector 0 0)) (pt (make-bytevector 16 0)) (out (aes-128-gcm-encrypt key iv aad pt)) (ct (car out)) (tag (cdr out)) (expected-ct (bytevector #x03 #x88 #xda #xce #x60 #xb6 #xa3 #x92 #xf3 #x28 #xc2 #xb9 #x71 #xb2 #xfe #x78)) (expected-tag (bytevector #xab #x6e #x47 #xd4 #x2c #xec #x13 #xbd #xf5 #x3a #x67 #xb2 #x12 #x57 #xbd #xdf))) (assert-equal expected-ct ct) (assert-equal expected-tag tag))) (test "round-trip: encrypt then decrypt yields original plaintext" (let* ((key (random-bytes 16)) (iv (random-bytes 12)) (aad "irrelevant aad") (pt "Hello, push subscriber. This is a longer-than-16-byte test message.") (out (aes-128-gcm-encrypt key iv aad pt)) (ct (car out)) (tag (cdr out)) (rt (aes-128-gcm-decrypt key iv aad ct tag))) (assert-true (bytevector? rt)) ;; Compare bytes against the input string. (assert-equal (string-length pt) (bytevector-length rt)) (let loop ((i 0)) (cond ((>= i (bytevector-length rt)) #t) (else (assert-equal (char->integer (string-ref pt i)) (bytevector-u8-ref rt i)) (loop (+ i 1))))))) (test "decrypt with tampered tag fails (returns #f)" (let* ((key (random-bytes 16)) (iv (random-bytes 12)) (aad (make-bytevector 0 0)) (pt "tagcheck") (out (aes-128-gcm-encrypt key iv aad pt)) (ct (car out)) (tag (cdr out))) (bytevector-u8-set! tag 0 (bitwise-xor (bytevector-u8-ref tag 0) #x01)) (assert-false (aes-128-gcm-decrypt key iv aad ct tag)))) (test "decrypt with tampered AAD fails" (let* ((key (random-bytes 16)) (iv (random-bytes 12)) (aad-good "expected aad") (aad-bad "tampered aad") (pt "aadcheck") (out (aes-128-gcm-encrypt key iv aad-good pt)) (ct (car out)) (tag (cdr out))) (assert-false (aes-128-gcm-decrypt key iv aad-bad ct tag)))) (test "decrypt with tampered ciphertext fails" (let* ((key (random-bytes 16)) (iv (random-bytes 12)) (aad (make-bytevector 0 0)) (pt "ctcheck-message-here") (out (aes-128-gcm-encrypt key iv aad pt)) (ct (car out)) (tag (cdr out))) (bytevector-u8-set! ct 0 (bitwise-xor (bytevector-u8-ref ct 0) #x55)) (assert-false (aes-128-gcm-decrypt key iv aad ct tag)))));; ============================================================;; RFC 8291 § 5 — Web Push end-to-end vector;; ============================================================;;;; The reference example exercises ECDH-P256 + HKDF-SHA256 +;; AES-128-GCM as composed for `aes128gcm` Content-Encoding. We;; reproduce the steps from § 3.1 / § 3.4 against the inputs in;; § 5 and verify the ciphertext + tag match. This is the most;; load-bearing KAT in this module — Web Push is the whole point;; of v0.15.1.;;;; Inputs (RFC 8291 § 5):;;;; plaintext = "When I grow up, I want to be a watermelon";; IKM = ECDH(as_priv, ua_pub) [ECE-IKM, §3.4];; salt = 16 random bytes, fixed in vector;; recordsize = 4096;;;; Where (from §5):;; ua_priv = q4yBd6S0FsYXqdvYJgcWGw;; (base64url; 32 bytes);; ua_pub (p256dh);; = BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcx;; aOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4;; (base64url; 65 bytes uncompressed P-256 point);; auth_secret = BTBZMqHH6r4Tts7J_aSIgg (16 bytes);; as_priv = yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRwShowing the first 500 of 648 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.