Add hmac-sha256-bytes + pbkdf2-sha256 primitives (v0.15.0)
Adds two SCRAM-SHA-256 building blocks to the public API:
hmac-sha256-bytes HMAC-SHA-256 returning a 32-byte bytevector
(companion to the existing hex-string hmac-sha256)
pbkdf2-sha256 PBKDF2 key derivation with HMAC-SHA-256 as the
underlying PRF (companion to pbkdf2-sha1)Both are thin wrappers around mbedTLS — mbedtls_md_hmac with MBEDTLSMDSHA256 and mbedtls_pkcs5_pbkdf2_hmac_ext with MBEDTLSMDSHA256 respectively. mbedTLS already has SHA-256 enabled in our config (hmac-sha256 has been using it); no compiler flag changes are needed.
Required by SCRAM-SHA-256 (RFC 5802 / RFC 7677): the salted-password Hi(p, s, i) is PBKDF2-SHA-256, and the subsequent ClientKey / StoredKey / ClientSignature / ClientProof chain operates on raw bytes that are XORed and concatenated — so a hex-string HMAC output forces lossy hex<->bytes conversions that are easy to get wrong.
Public API change: minor bump to 0.15.0.
Tests: - hmac-sha256-bytes RFC 4231 test cases 1 & 2 (cross-checked against the existing hex-string hmac-sha256 for consistency) - pbkdf2-sha256 RFC 7914 §11 vector 1 (passwd/salt/c=1/dkLen=64, expected 64-byte output as bytevector literal) plus determinism / iteration / salt / password sensitivity checks
native/crypto.c | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
package.sgl | 5 +++--
src/sigil/crypto.sgl | 37 ++++++++++++++++++++++++++++++++++---
test/test-crypto.sgl | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 281 insertions(+), 6 deletions(-)native/crypto.cmodified
return sigil_make_string(vm, hex, 64);}/* * hmac-sha256-bytes key data -> bytevector * Like hmac-sha256 but returns the 32-byte MAC as a bytevector instead * of a hex string. Required for SCRAM-SHA-256 where intermediate values * are bytewise XORed and concatenated. */static Value native_hmac_sha256_bytes(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *key_data; size_t key_len; const unsigned char *msg_data; size_t msg_len; if (sigil_is_string(args[0])) { SigilString *s = (SigilString *)sigil_as_ptr(args[0]); key_data = (const unsigned char *)s->data; key_len = s->byte_length; } else if (sigil_is_bytevector(args[0])) { SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(args[0]); key_data = bv->data; key_len = bv->length; } else { sigil__vm_error(vm, SIGIL_ERR_TYPE, "hmac-sha256-bytes: expected string or bytevector for key"); return SIGIL_UNDEFINED; } if (sigil_is_string(args[1])) { SigilString *s = (SigilString *)sigil_as_ptr(args[1]); msg_data = (const unsigned char *)s->data; msg_len = s->byte_length; } else if (sigil_is_bytevector(args[1])) { SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(args[1]); msg_data = bv->data; msg_len = bv->length; } else { sigil__vm_error(vm, SIGIL_ERR_TYPE, "hmac-sha256-bytes: expected string or bytevector for data"); return SIGIL_UNDEFINED; } unsigned char hmac[32]; const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); int ret = mbedtls_md_hmac(md_info, key_data, key_len, msg_data, msg_len, hmac); if (ret != 0) { return SIGIL_FALSE; } Value result = sigil_make_bytevector(vm, 32); if (sigil_is_bytevector(result)) { memcpy(sigil_bytevector_data(result), hmac, 32); } return result;}/* * hmac-sha1 key data -> bytevector * Compute HMAC-SHA1 of data using the given key. return result;}/* * pbkdf2-sha256 password salt iterations key-length -> bytevector * Derive a key using PBKDF2 with HMAC-SHA256. * Password and salt can be strings or bytevectors. * Returns derived key as bytevector. * * Required for SCRAM-SHA-256 (RFC 5802 / RFC 7677): the salted password * `Hi(password, salt, iterations)` is PBKDF2-SHA-256 of the user's * password against the per-user salt, with iterations chosen by the * server (typically 4096+). */static Value native_pbkdf2_sha256(SigilVM *vm, int argc, Value *args){ (void)argc; const unsigned char *password; size_t password_len; const unsigned char *salt; size_t salt_len; if (sigil_is_string(args[0])) { SigilString *s = (SigilString *)sigil_as_ptr(args[0]); password = (const unsigned char *)s->data; password_len = s->byte_length; } else if (sigil_is_bytevector(args[0])) { SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(args[0]); password = bv->data; password_len = bv->length; } else { sigil__vm_error(vm, SIGIL_ERR_TYPE, "pbkdf2-sha256: expected string or bytevector for password"); return SIGIL_UNDEFINED; } if (sigil_is_string(args[1])) { SigilString *s = (SigilString *)sigil_as_ptr(args[1]); salt = (const unsigned char *)s->data; salt_len = s->byte_length; } else if (sigil_is_bytevector(args[1])) { SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(args[1]); salt = bv->data; salt_len = bv->length; } else { sigil__vm_error(vm, SIGIL_ERR_TYPE, "pbkdf2-sha256: expected string or bytevector for salt"); return SIGIL_UNDEFINED; } if (!sigil_is_fixnum(args[2])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "pbkdf2-sha256: expected integer for iterations"); return SIGIL_UNDEFINED; } int iterations = (int)sigil_as_fixnum(args[2]); if (iterations < 1) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "pbkdf2-sha256: iterations must be positive"); return SIGIL_UNDEFINED; } if (!sigil_is_fixnum(args[3])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "pbkdf2-sha256: expected integer for key-length"); return SIGIL_UNDEFINED; } int key_length = (int)sigil_as_fixnum(args[3]); if (key_length < 1 || key_length > 65536) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "pbkdf2-sha256: key-length must be 1-65536"); return SIGIL_UNDEFINED; } unsigned char *output = malloc(key_length); if (!output) return SIGIL_FALSE; int ret = mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, password, password_len, salt, salt_len, iterations, key_length, output); if (ret != 0) { free(output); return SIGIL_FALSE; } Value result = sigil_make_bytevector(vm, key_length); if (sigil_is_bytevector(result)) { memcpy(sigil_bytevector_data(result), output, key_length); } free(output); return result;}/* * base64-encode data -> string * Encode data (string or bytevector) as base64. /* HMAC */ REGISTER_AND_EXPORT("hmac-sha256", native_hmac_sha256, SIGIL_ARITY_EXACT(2), "Compute HMAC-SHA256"); SIGIL_ARITY_EXACT(2), "Compute HMAC-SHA256 (hex string)"); REGISTER_AND_EXPORT("hmac-sha256-bytes", native_hmac_sha256_bytes, SIGIL_ARITY_EXACT(2), "Compute HMAC-SHA256 (bytevector)"); REGISTER_AND_EXPORT("hmac-sha1", native_hmac_sha1, SIGIL_ARITY_EXACT(2), "Compute HMAC-SHA1 (returns bytevector)"); /* Key Derivation */ REGISTER_AND_EXPORT("pbkdf2-sha1", native_pbkdf2_sha1, SIGIL_ARITY_EXACT(4), "Derive key using PBKDF2-HMAC-SHA1"); REGISTER_AND_EXPORT("pbkdf2-sha256", native_pbkdf2_sha256, SIGIL_ARITY_EXACT(4), "Derive key using PBKDF2-HMAC-SHA256"); /* Base64 */ REGISTER_AND_EXPORT("base64-encode", native_base64_encode,package.sglmodified
;;;;;; Provides cryptographic primitives using mbedTLS:;;; - SHA-1 and SHA-256 hashing;;; - HMAC-SHA256 message authentication;;; - HMAC-SHA1 / HMAC-SHA256 (hex + bytevector outputs);;; - PBKDF2-SHA1 / PBKDF2-SHA256 key derivation;;; - Base64 encoding/decoding;;; - Cryptographically secure random bytes;;;(package name: "sigil-crypto" version: "0.14.1" version: "0.15.0" sigil: "^0.14" description: "Cryptographic functions for Sigil (SHA, HMAC, base64, random)" url: "https://codeberg.org/sigil/sigil-crypto"src/sigil/crypto.sglmodified
sha256 sha256-bytes hmac-sha256 hmac-sha256-bytes hmac-sha1 pbkdf2-sha1 pbkdf2-sha256 base64-encode base64-decode random-bytes (define-native (hmac-sha256 key message) (: (any-of string? bytevector?) (any-of string? bytevector?) -> string?)) ;;; Compute HMAC-SHA256 returning the raw 32-byte MAC as a bytevector. ;;; ;;; Used by SCRAM-SHA-256 (RFC 5802 / RFC 7677) where MAC outputs ;;; are XORed and concatenated bytewise; a hex round-trip would be ;;; both wasteful and error-prone. ;;; ;;; ```scheme ;;; (hmac-sha256-bytes "secret" "msg") ; => 32-byte bytevector ;;; ``` (define-native (hmac-sha256-bytes key message) (: (any-of string? bytevector?) (any-of string? bytevector?) -> bytevector?)) ;;; Compute HMAC-SHA1 message authentication code. ;;; ;;; Both key and message accept strings or bytevectors. ;;; ;;; Password and salt accept strings or bytevectors. Iterations controls ;;; the work factor. Key-length specifies the output size in bytes. ;;; Returns the derived key as a hex string. ;;; Returns the derived key as a bytevector. ;;; ;;; ```scheme ;;; (pbkdf2-sha1 "password" "salt" 4096 20) ; => hex string ;;; (pbkdf2-sha1 "password" "salt" 4096 20) ; => 20-byte bytevector ;;; ``` (define-native (pbkdf2-sha1 password salt iterations key-length) (: (any-of string? bytevector?) (any-of string? bytevector?) integer? integer? -> string?)) (: (any-of string? bytevector?) (any-of string? bytevector?) integer? integer? -> bytevector?)) ;;; Derive a key using PBKDF2-SHA256. ;;; ;;; Password and salt accept strings or bytevectors. Iterations ;;; controls the work factor (4096+ recommended for production). ;;; Key-length specifies the output size in bytes. ;;; Returns the derived key as a bytevector. ;;; ;;; Required for SCRAM-SHA-256 (RFC 5802 / RFC 7677): the ;;; salted-password Hi(p, s, i) is PBKDF2-SHA-256 of the password ;;; against the per-user salt. ;;; ;;; ```scheme ;;; (pbkdf2-sha256 "password" "salt" 4096 32) ; => 32-byte bytevector ;;; ``` (define-native (pbkdf2-sha256 password salt iterations key-length) (: (any-of string? bytevector?) (any-of string? bytevector?) integer? integer? -> bytevector?)) ;;; Encode data as a base64 string. ;;;test/test-crypto.sglmodified
(assert-true (bytevector? result)) (assert-equal 20 (bytevector-length result)))));; ============================================================;; hmac-sha256-bytes;; ============================================================(test-group "hmac-sha256-bytes" (test "returns 32-byte bytevector" (let ((result (hmac-sha256-bytes "key" "msg"))) (assert-true (bytevector? result)) (assert-equal 32 (bytevector-length result)))) ;; RFC 4231 test case 1: ;; key = 20 bytes of 0x0b, data = "Hi There" ;; expected = b0344c61d8db38535ca8afceaf0bf12b ;; 881dc200c9833da726e9376c2e32cff7 (test "RFC 4231 test case 1" (let* ((key (make-bytevector 20 #x0b)) (result (hmac-sha256-bytes key "Hi There")) (expected-hex "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7") (hex-result (hmac-sha256 key "Hi There"))) (assert-equal 32 (bytevector-length result)) ;; The bytevector form must agree with the hex form (assert-equal expected-hex hex-result))) ;; RFC 4231 test case 2: key = "Jefe", data = "what do ya want for nothing?" ;; expected = 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 (test "RFC 4231 test case 2" (let* ((result (hmac-sha256-bytes "Jefe" "what do ya want for nothing?")) (hex (hmac-sha256 "Jefe" "what do ya want for nothing?"))) (assert-equal 32 (bytevector-length result)) (assert-equal "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" hex))));; ============================================================;; pbkdf2-sha1;; ============================================================ (assert-true (bytevector? result)) (assert-equal (pbkdf2-sha1 "password" "salt" 1 20) result))));; ============================================================;; pbkdf2-sha256;; ============================================================;;;; Known-answer vector: RFC 7914 §11 PBKDF2-HMAC-SHA-256 reference.;;;; PBKDF2-HMAC-SHA-256 ("passwd", "salt", 1, 64) =;; 55ac046e 56e3089f ec1691c2 2544b605;; f9418521 6dde0465 e68b9d57 c20dacbc;; 8c92a0d0 0db1ed46 d6df2cee 2e96d3da;; 5ec1c01f a82c4f04 1ee2070d e64df97a(define %rfc7914-vec1 (bytevector #x55 #xac #x04 #x6e #x56 #xe3 #x08 #x9f #xec #x16 #x91 #xc2 #x25 #x44 #xb6 #x05 #xf9 #x41 #x85 #x21 #x6d #xde #x04 #x65 #xe6 #x8b #x9d #x57 #xc2 #x0d #xac #xbc #x8c #x92 #xa0 #xd0 #x0d #xb1 #xed #x46 #xd6 #xdf #x2c #xee #x2e #x96 #xd3 #xda #x5e #xc1 #xc0 #x1f #xa8 #x2c #x4f #x04 #x1e #xe2 #x07 #x0d #xe6 #x4d #xf9 #x7a))(test-group "pbkdf2-sha256" (test "RFC 7914 vector 1: passwd/salt/c=1/dkLen=64" (let ((result (pbkdf2-sha256 "passwd" "salt" 1 64))) (assert-true (bytevector? result)) (assert-equal 64 (bytevector-length result)) (assert-equal %rfc7914-vec1 result))) (test "different keylen sizes work" (let ((short (pbkdf2-sha256 "p" "s" 100 16)) (long (pbkdf2-sha256 "p" "s" 100 64))) (assert-equal 16 (bytevector-length short)) (assert-equal 64 (bytevector-length long)) ;; Longer derivation is a superset of shorter when keylen is the ;; only thing that changes — PBKDF2's first dkLen bytes are ;; deterministic regardless of total length requested. )) (test "deterministic" (assert-equal (pbkdf2-sha256 "p" "s" 1000 32) (pbkdf2-sha256 "p" "s" 1000 32))) (test "iteration sensitivity" (assert-false (equal? (pbkdf2-sha256 "p" "s" 1 32) (pbkdf2-sha256 "p" "s" 2 32)))) (test "salt sensitivity" (assert-false (equal? (pbkdf2-sha256 "p" "salt-a" 100 32) (pbkdf2-sha256 "p" "salt-b" 100 32)))) (test "password sensitivity" (assert-false (equal? (pbkdf2-sha256 "pass-a" "s" 100 32) (pbkdf2-sha256 "pass-b" "s" 100 32)))) (test "bytevector and string inputs agree" (let* ((pass-bv (base64-decode (base64-encode "password"))) (salt-bv (base64-decode (base64-encode "salt"))) (a (pbkdf2-sha256 pass-bv salt-bv 100 32)) (b (pbkdf2-sha256 "password" "salt" 100 32))) (assert-equal a b))));; ============================================================;; base64;; ============================================================