fix-forward PR #1: MPI UB, doc/behavior mismatches, non-negative shift args
Closes the four classes of issue surfaced during post-merge review of trevarj's #1 (feat: ripemd160, hmac-sha512-bytes, mbedTLS MPI bignum primitives) and bumps sigil-crypto for release.
native/crypto.c - MPIBINOP macro: declare `Value result = SIGILFALSE; at the top so error paths can't read uninitialized memory; drop the (result != SIGILUNDEFINED) ? result : SIGILFALSE ternary at return. Same bug existed in nativempimod and nativempimodadd (compiler -Wsometimes-uninitialized confirmed both); applied the same fix to all three so the UB issue class is closed. - mpi-shift-l: reject negative bits or size (was casting via (sizet)sigilasfixnum(...) and underflowing to a huge value that mbedtls_mpi_shift_l would then try to grow into). - mpi-shift-r: same, for bits`.
src/sigil/crypto.sgl - mpi-sub docstring: was "wraps modulo 2^(8size) when negative". mbedtls_mpi_write_binary writes only the magnitude and discards the sign, so a negative result surfaces as |a - b| — not as a two's-complement wraparound. Rewrote to describe absolute-value semantics and point callers needing modular wraparound at mpi-mod-add against a precomputed `2^(8size) modulus. - mpi-shift-l docstring: was "excess high bits are truncated". mbedtlsmpishiftl grows the MPI to fit; on overflow mpiwritebv` returns MBEDTLSERRMPIBUFFERTOOSMALL and the function returns #f. Rewrote to describe the actual #f-on-overflow semantics and the new non-negative bits/size requirement; return type now (any-of bytevector? boolean?). - Added a CONSTANT-TIME WARNING header before the mpi- block: these primitives leak operand-dependent timing and must not be applied to secret scalars; for ECDSA/ECDH on P-256, prefer the existing `ecdsa-p256- / ecdh-p256-*` primitives.
test/test-crypto.sgl - mpi-sub: cover (3 - 5, size=4) returning #u8(0 0 0 2) — the magnitude, NOT a 2^32 wraparound. - mpi-shift-l: cover (1 << 32, size=4) returning #f when the shifted value overflows size. - mpi-shift-l: cover negative bits and negative size raising an error (matches the new sigil_vmerror guards).
package.sgl - sigil dep ^0.14 -> ^0.15.0 - version 0.15.3 -> 0.16.0 (minor-as-major: requiring sigil ^0.15 is a breaking change for consumers).
sigil.lock regenerated by sigil deps install against the new dep.
native/crypto.c | 38 +++++++++++++++++++++++++++++---------
package.sgl | 4 ++--
sigil.lock | 52 ++++++++++++++++++++++++++++++++++++----------------
src/sigil/crypto.sgl | 26 ++++++++++++++++++++++----
test/test-crypto.sgl | 23 +++++++++++++++++++++--
5 files changed, 110 insertions(+), 33 deletions(-)native/crypto.cmodified
size_t size = (size_t)sigil_as_fixnum(args[2]); \ mbedtls_mpi A, B, R; \ mbedtls_mpi_init(&A); mbedtls_mpi_init(&B); mbedtls_mpi_init(&R); \ Value result = SIGIL_FALSE; \ if (mpi_read_bv(&A, args[0]) != 0) goto cleanup; \ if (mpi_read_bv(&B, args[1]) != 0) goto cleanup; \ if (op_fn(&R, &A, &B) != 0) goto cleanup; \ Value result = mpi_write_bv(vm, &R, size); \ result = mpi_write_bv(vm, &R, size); \cleanup: \ mbedtls_mpi_free(&R); mbedtls_mpi_free(&B); mbedtls_mpi_free(&A); \ return ((result != SIGIL_UNDEFINED) ? result : SIGIL_FALSE); \ return result; \}MPI_BINOP(add, mbedtls_mpi_add_mpi) mbedtls_mpi A, N, R; mbedtls_mpi_init(&A); mbedtls_mpi_init(&N); mbedtls_mpi_init(&R); Value result = SIGIL_FALSE; if (mpi_read_bv(&A, args[0]) != 0) goto cleanup; if (mpi_read_bv(&N, args[1]) != 0) goto cleanup; if (mbedtls_mpi_mod_mpi(&R, &A, &N) != 0) goto cleanup; Value result = mpi_write_bv(vm, &R, size); result = mpi_write_bv(vm, &R, size);cleanup: mbedtls_mpi_free(&R); mbedtls_mpi_free(&N); mbedtls_mpi_free(&A); return (result != SIGIL_UNDEFINED) ? result : SIGIL_FALSE; return result;}/* mbedtls_mpi_init(&A); mbedtls_mpi_init(&B); mbedtls_mpi_init(&N); mbedtls_mpi_init(&R); Value result = SIGIL_FALSE; if (mpi_read_bv(&A, args[0]) != 0) goto cleanup; if (mpi_read_bv(&B, args[1]) != 0) goto cleanup; if (mpi_read_bv(&N, args[2]) != 0) goto cleanup; if (mbedtls_mpi_add_mpi(&R, &A, &B) != 0) goto cleanup; if (mbedtls_mpi_mod_mpi(&R, &R, &N) != 0) goto cleanup; Value result = mpi_write_bv(vm, &R, size); result = mpi_write_bv(vm, &R, size);cleanup: mbedtls_mpi_free(&R); mbedtls_mpi_free(&N); mbedtls_mpi_free(&B); mbedtls_mpi_free(&A); return (result != SIGIL_UNDEFINED) ? result : SIGIL_FALSE; return result;}/* return SIGIL_UNDEFINED; } size_t bits = (size_t)sigil_as_fixnum(args[1]); size_t size = (size_t)sigil_as_fixnum(args[2]); int64_t bits_in = sigil_as_fixnum(args[1]); int64_t size_in = sigil_as_fixnum(args[2]); if (bits_in < 0 || size_in < 0) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "mpi-shift-l: bits and size must be non-negative"); return SIGIL_UNDEFINED; } size_t bits = (size_t)bits_in; size_t size = (size_t)size_in; mbedtls_mpi A; mbedtls_mpi_init(&A); return SIGIL_UNDEFINED; } size_t bits = (size_t)sigil_as_fixnum(args[1]); int64_t bits_in = sigil_as_fixnum(args[1]); if (bits_in < 0) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "mpi-shift-r: bits must be non-negative"); return SIGIL_UNDEFINED; } size_t bits = (size_t)bits_in; mbedtls_mpi A; mbedtls_mpi_init(&A);package.sglmodified
(package name: "sigil-crypto" version: "0.15.3" sigil: "^0.14" version: "0.16.0" sigil: "^0.15.0" 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"sigil.lockmodified
;; Auto-generated by sigil deps install. Do not edit.(lock (package name: "sigil-lib" url: "codeberg:sigil/sigil-lang" ref: "^0.14" sha: "152ea26c05b73c5d003b246a008d0a79ed0c42d0" package-selector: "sigil-lib" version: "0.14.17") url: "codeberg:sigil/sigil" ref: "^0.15.0" sha: "7e7bc93520d4f2a413d532c23a45ae74deab5774" package-selector: "sigil-lib") (package name: "sigil-stdlib" url: "codeberg:sigil/sigil-lang" ref: "^0.14" sha: "152ea26c05b73c5d003b246a008d0a79ed0c42d0" package-selector: "sigil-stdlib" version: "0.14.17") url: "codeberg:sigil/sigil" ref: "^0.15.0" sha: "7e7bc93520d4f2a413d532c23a45ae74deab5774" package-selector: "sigil-stdlib") (package name: "sigil-test" url: "codeberg:sigil/sigil-test" ref: "^0.13.1" sha: "cc8ccbc04b28d5de8c1253513e246547a34e7a73" ref: "^0.14.0" sha: "080fd3b59e56d5bc185ff1f4e2ef06bfb5b69661" package-selector: "sigil-test" version: "0.13.1") version: "0.14.0") (package name: "sigil-test-runner" url: "codeberg:sigil/sigil-test" ref: "^0.13.1" sha: "cc8ccbc04b28d5de8c1253513e246547a34e7a73" ref: "^0.14.0" sha: "080fd3b59e56d5bc185ff1f4e2ef06bfb5b69661" package-selector: "sigil-test-runner" version: "0.13.1") version: "0.14.2") (package name: "sigil-package" url: "codeberg:sigil/sigil-build" ref: "^0.14.0" sha: "1f87c151fe9017fe93fc1ded80b63f86488c0e30" package-selector: "sigil-package" version: "0.14.4") (package name: "sigil-build" url: "codeberg:sigil/sigil-build" ref: "^0.14.0" sha: "1f87c151fe9017fe93fc1ded80b63f86488c0e30" package-selector: "sigil-build" version: "0.14.4") (package name: "sigil-ansi" url: "codeberg:sigil/sigil" ref: "^0.13.1" ref: "^0.13.1" sha: "811b63925399a663e4b7dd25fb0813d59d33f557" version: "0.13.1") (package name: "sigil-version" url: "codeberg:sigil/sigil-version" ref: "^0.13.1" sha: "f3a59bf7d0da81ad5206df5b2719e0a27b5b5904" version: "0.13.1") (package name: "sigil-git" url: "codeberg:sigil/sigil-git" ref: "^0.13.1" sha: "f7750b3e79a560571987f55d191ea8b36ca92794" version: "0.13.1"))src/sigil/crypto.sglmodified
(define-native (random-bytes count) (: integer? -> bytevector?)) ;; --------------------------------------------------------------- ;; mbedTLS MPI — Big Integer Arithmetic ;; ;; CONSTANT-TIME WARNING: these MPI primitives are NOT constant-time. ;; `mpi-inv-mod`, `mpi-mod`, `mpi-div`, and others leak operand- ;; dependent timing. Do not apply directly to secret scalars ;; (e.g. BIP32 child key derivation, EC scalar multiplication). ;; For ECDSA/ECDH on P-256, prefer the existing `ecdsa-p256-*` ;; and `ecdh-p256-*` primitives, which use mbedTLS's hardened paths. ;; --------------------------------------------------------------- ;;; Add two bytevectors as big integers. ;;; ;;; Bytevectors are interpreted as unsigned big-endian integers. ;;; Subtract two bytevectors as big integers. ;;; ;;; Result wraps modulo 2^(8*size) when negative. ;;; Bytevectors are interpreted as unsigned big-endian integers. ;;; The result is written right-padded to `size` bytes as the ;;; absolute value of the difference: when `a < b`, the returned ;;; bytevector encodes `|a - b|`, NOT a two's-complement wraparound. ;;; For modular wraparound semantics, use `mpi-mod-add` against a ;;; precomputed `2^(8*size)` modulus. (define-native (mpi-sub a-bv b-bv size) (: bytevector? bytevector? integer? -> bytevector?)) ;;; Left-shift a bytevectored big integer by `bits` positions. ;;; ;;; The result is written right-padded to `size` bytes; excess ;;; high bits are truncated. ;;; The result is written right-padded to `size` bytes. Returns ;;; `#f` when the shifted value does not fit in `size` bytes — ;;; high bits are NOT truncated. `bits` and `size` must be ;;; non-negative; passing a negative fixnum signals a type error. ;;; ;;; ```scheme ;;; (mpi-shift-l #u8(0 0 0 1) 3 4) ; => #u8(0 0 0 8) ;;; ``` (define-native (mpi-shift-l a-bv bits size) (: bytevector? integer? integer? -> bytevector?)) (: bytevector? integer? integer? -> (any-of bytevector? boolean?))) ;;; Right-shift a bytevectored big integer by `bits` positions. ;;;test/test-crypto.sglmodified
(let ((a #u8(0 0 0 5)) (b #u8(0 0 0 3)) (result (mpi-sub a b 4))) (assert-equal 2 (bytevector-u8-ref result 3))))) (assert-equal 2 (bytevector-u8-ref result 3)))) (test "3 - 5 yields |3 - 5| = 2 (absolute value, NOT modular wraparound)" ;; mbedtls_mpi_write_binary writes the magnitude and discards ;; sign — so 3 - 5 = -2 surfaces as the bytevector for 2, not ;; as a 2^32 wraparound (which would be #u8(255 255 255 254)). (assert-equal #u8(0 0 0 2) (mpi-sub #u8(0 0 0 3) #u8(0 0 0 5) 4))))(test-group "mpi-mul" (test "3 * 4 = 12 (big-endian, 4 bytes)"(test-group "mpi-shift-l" (test "1 << 3 = 8 (big-endian, 4 bytes)" (let ((result (mpi-shift-l #u8(0 0 0 1) 3 4))) (assert-equal 8 (bytevector-u8-ref result 3))))) (assert-equal 8 (bytevector-u8-ref result 3)))) (test "shifted value that does not fit in `size` bytes returns #f" ;; 1 << 32 = 0x1_0000_0000 needs 5 bytes; size=4 cannot hold it, ;; so the underlying mbedtls_mpi_write_binary signals ;; MBEDTLS_ERR_MPI_BUFFER_TOO_SMALL and the function returns #f. (assert-false (mpi-shift-l #u8(0 0 0 1) 32 4))) (test "negative bits raises an error" (assert-error (mpi-shift-l #u8(0 0 0 1) -1 4))) (test "negative size raises an error" (assert-error (mpi-shift-l #u8(0 0 0 1) 1 -1))))(test-group "mpi-shift-r" (test "8 >> 3 = 1 (big-endian, 4 bytes)"