Commit303daf38Recorded4 May 2026Repositorysigil-crypto

fix-forward PR #1: MPI UB, doc/behavior mismatches, non-negative shift args

Message

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.

Changed
 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(-)
Diff
native/crypto.cmodified
@@ -922,13 +922,14 @@ static Value native_mpi_##method_name(SigilVM *vm, int argc, Value *args) \
922
size_t size = (size_t)sigil_as_fixnum(args[2]); \
923
mbedtls_mpi A, B, R; \
924
mbedtls_mpi_init(&A); mbedtls_mpi_init(&B); mbedtls_mpi_init(&R); \
+925
Value result = SIGIL_FALSE; \
926
if (mpi_read_bv(&A, args[0]) != 0) goto cleanup; \
927
if (mpi_read_bv(&B, args[1]) != 0) goto cleanup; \
928
if (op_fn(&R, &A, &B) != 0) goto cleanup; \
928
Value result = mpi_write_bv(vm, &R, size); \
+929
result = mpi_write_bv(vm, &R, size); \
930
cleanup: \
931
mbedtls_mpi_free(&R); mbedtls_mpi_free(&B); mbedtls_mpi_free(&A); \
931
return ((result != SIGIL_UNDEFINED) ? result : SIGIL_FALSE); \
+932
return result; \
933
}
934
935
MPI_BINOP(add, mbedtls_mpi_add_mpi)
@@ -998,15 +999,17 @@ static Value native_mpi_mod(SigilVM *vm, int argc, Value *args)
999
mbedtls_mpi A, N, R;
1000
mbedtls_mpi_init(&A); mbedtls_mpi_init(&N); mbedtls_mpi_init(&R);
1001
+1002
Value result = SIGIL_FALSE;
+1003
1004
if (mpi_read_bv(&A, args[0]) != 0) goto cleanup;
1005
if (mpi_read_bv(&N, args[1]) != 0) goto cleanup;
1006
if (mbedtls_mpi_mod_mpi(&R, &A, &N) != 0) goto cleanup;
1007
1005
Value result = mpi_write_bv(vm, &R, size);
+1008
result = mpi_write_bv(vm, &R, size);
1009
1010
cleanup:
1011
mbedtls_mpi_free(&R); mbedtls_mpi_free(&N); mbedtls_mpi_free(&A);
1009
return (result != SIGIL_UNDEFINED) ? result : SIGIL_FALSE;
+1012
return result;
1013
}
1014
1015
/*
@@ -1033,18 +1036,20 @@ static Value native_mpi_mod_add(SigilVM *vm, int argc, Value *args)
1036
mbedtls_mpi_init(&A); mbedtls_mpi_init(&B);
1037
mbedtls_mpi_init(&N); mbedtls_mpi_init(&R);
1038
+1039
Value result = SIGIL_FALSE;
+1040
1041
if (mpi_read_bv(&A, args[0]) != 0) goto cleanup;
1042
if (mpi_read_bv(&B, args[1]) != 0) goto cleanup;
1043
if (mpi_read_bv(&N, args[2]) != 0) goto cleanup;
1044
if (mbedtls_mpi_add_mpi(&R, &A, &B) != 0) goto cleanup;
1045
if (mbedtls_mpi_mod_mpi(&R, &R, &N) != 0) goto cleanup;
1046
1042
Value result = mpi_write_bv(vm, &R, size);
+1047
result = mpi_write_bv(vm, &R, size);
1048
1049
cleanup:
1050
mbedtls_mpi_free(&R); mbedtls_mpi_free(&N);
1051
mbedtls_mpi_free(&B); mbedtls_mpi_free(&A);
1047
return (result != SIGIL_UNDEFINED) ? result : SIGIL_FALSE;
+1052
return result;
1053
}
1054
1055
/*
@@ -1149,8 +1154,16 @@ static Value native_mpi_shift_l(SigilVM *vm, int argc, Value *args)
1154
return SIGIL_UNDEFINED;
1155
}
1156
1152
size_t bits = (size_t)sigil_as_fixnum(args[1]);
1153
size_t size = (size_t)sigil_as_fixnum(args[2]);
+1157
int64_t bits_in = sigil_as_fixnum(args[1]);
+1158
int64_t size_in = sigil_as_fixnum(args[2]);
+1159
if (bits_in < 0 || size_in < 0) {
+1160
sigil__vm_error(vm, SIGIL_ERR_TYPE,
+1161
"mpi-shift-l: bits and size must be non-negative");
+1162
return SIGIL_UNDEFINED;
+1163
}
+1164
+1165
size_t bits = (size_t)bits_in;
+1166
size_t size = (size_t)size_in;
1167
1168
mbedtls_mpi A;
1169
mbedtls_mpi_init(&A);
@@ -1186,7 +1199,14 @@ static Value native_mpi_shift_r(SigilVM *vm, int argc, Value *args)
1199
return SIGIL_UNDEFINED;
1200
}
1201
1189
size_t bits = (size_t)sigil_as_fixnum(args[1]);
+1202
int64_t bits_in = sigil_as_fixnum(args[1]);
+1203
if (bits_in < 0) {
+1204
sigil__vm_error(vm, SIGIL_ERR_TYPE,
+1205
"mpi-shift-r: bits must be non-negative");
+1206
return SIGIL_UNDEFINED;
+1207
}
+1208
+1209
size_t bits = (size_t)bits_in;
1210
1211
mbedtls_mpi A;
1212
mbedtls_mpi_init(&A);
package.sglmodified
@@ -15,8 +15,8 @@
15
16
(package
17
name: "sigil-crypto"
18
version: "0.15.3"
19
sigil: "^0.14"
+18
version: "0.16.0"
+19
sigil: "^0.15.0"
20
description: "Cryptographic functions for Sigil (SHA, HMAC, ECDSA, ECDH, AES-GCM, HKDF, base64, random)"
21
url: "https://codeberg.org/sigil/sigil-crypto"
22
license: "BSD-3-Clause"
sigil.lockmodified
@@ -1,29 +1,39 @@
1
;; Auto-generated by sigil deps install. Do not edit.
2
(lock
3
(package name: "sigil-lib"
4
url: "codeberg:sigil/sigil-lang"
5
ref: "^0.14"
6
sha: "152ea26c05b73c5d003b246a008d0a79ed0c42d0"
7
package-selector: "sigil-lib"
8
version: "0.14.17")
+4
url: "codeberg:sigil/sigil"
+5
ref: "^0.15.0"
+6
sha: "7e7bc93520d4f2a413d532c23a45ae74deab5774"
+7
package-selector: "sigil-lib")
8
(package name: "sigil-stdlib"
10
url: "codeberg:sigil/sigil-lang"
11
ref: "^0.14"
12
sha: "152ea26c05b73c5d003b246a008d0a79ed0c42d0"
13
package-selector: "sigil-stdlib"
14
version: "0.14.17")
+9
url: "codeberg:sigil/sigil"
+10
ref: "^0.15.0"
+11
sha: "7e7bc93520d4f2a413d532c23a45ae74deab5774"
+12
package-selector: "sigil-stdlib")
13
(package name: "sigil-test"
14
url: "codeberg:sigil/sigil-test"
17
ref: "^0.13.1"
18
sha: "cc8ccbc04b28d5de8c1253513e246547a34e7a73"
+15
ref: "^0.14.0"
+16
sha: "080fd3b59e56d5bc185ff1f4e2ef06bfb5b69661"
17
package-selector: "sigil-test"
20
version: "0.13.1")
+18
version: "0.14.0")
19
(package name: "sigil-test-runner"
20
url: "codeberg:sigil/sigil-test"
23
ref: "^0.13.1"
24
sha: "cc8ccbc04b28d5de8c1253513e246547a34e7a73"
+21
ref: "^0.14.0"
+22
sha: "080fd3b59e56d5bc185ff1f4e2ef06bfb5b69661"
23
package-selector: "sigil-test-runner"
26
version: "0.13.1")
+24
version: "0.14.2")
+25
(package name: "sigil-package"
+26
url: "codeberg:sigil/sigil-build"
+27
ref: "^0.14.0"
+28
sha: "1f87c151fe9017fe93fc1ded80b63f86488c0e30"
+29
package-selector: "sigil-package"
+30
version: "0.14.4")
+31
(package name: "sigil-build"
+32
url: "codeberg:sigil/sigil-build"
+33
ref: "^0.14.0"
+34
sha: "1f87c151fe9017fe93fc1ded80b63f86488c0e30"
+35
package-selector: "sigil-build"
+36
version: "0.14.4")
37
(package name: "sigil-ansi"
38
url: "codeberg:sigil/sigil"
39
ref: "^0.13.1"
@@ -34,4 +44,14 @@
44
ref: "^0.13.1"
45
sha: "811b63925399a663e4b7dd25fb0813d59d33f557"
46
version: "0.13.1")
+47
(package name: "sigil-version"
+48
url: "codeberg:sigil/sigil-version"
+49
ref: "^0.13.1"
+50
sha: "f3a59bf7d0da81ad5206df5b2719e0a27b5b5904"
+51
version: "0.13.1")
+52
(package name: "sigil-git"
+53
url: "codeberg:sigil/sigil-git"
+54
ref: "^0.13.1"
+55
sha: "f7750b3e79a560571987f55d191ea8b36ca92794"
+56
version: "0.13.1")
57
)
src/sigil/crypto.sglmodified
@@ -201,6 +201,17 @@
201
(define-native (random-bytes count)
202
(: integer? -> bytevector?))
203
+204
;; ---------------------------------------------------------------
+205
;; mbedTLS MPI — Big Integer Arithmetic
+206
;;
+207
;; CONSTANT-TIME WARNING: these MPI primitives are NOT constant-time.
+208
;; `mpi-inv-mod`, `mpi-mod`, `mpi-div`, and others leak operand-
+209
;; dependent timing. Do not apply directly to secret scalars
+210
;; (e.g. BIP32 child key derivation, EC scalar multiplication).
+211
;; For ECDSA/ECDH on P-256, prefer the existing `ecdsa-p256-*`
+212
;; and `ecdh-p256-*` primitives, which use mbedTLS's hardened paths.
+213
;; ---------------------------------------------------------------
+214
215
;;; Add two bytevectors as big integers.
216
;;;
217
;;; Bytevectors are interpreted as unsigned big-endian integers.
@@ -215,7 +226,12 @@
226
227
;;; Subtract two bytevectors as big integers.
228
;;;
218
;;; Result wraps modulo 2^(8*size) when negative.
+229
;;; Bytevectors are interpreted as unsigned big-endian integers.
+230
;;; The result is written right-padded to `size` bytes as the
+231
;;; absolute value of the difference: when `a < b`, the returned
+232
;;; bytevector encodes `|a - b|`, NOT a two's-complement wraparound.
+233
;;; For modular wraparound semantics, use `mpi-mod-add` against a
+234
;;; precomputed `2^(8*size)` modulus.
235
(define-native (mpi-sub a-bv b-bv size)
236
(: bytevector? bytevector? integer? -> bytevector?))
237
@@ -277,14 +293,16 @@
293
294
;;; Left-shift a bytevectored big integer by `bits` positions.
295
;;;
280
;;; The result is written right-padded to `size` bytes; excess
281
;;; high bits are truncated.
+296
;;; The result is written right-padded to `size` bytes. Returns
+297
;;; `#f` when the shifted value does not fit in `size` bytes —
+298
;;; high bits are NOT truncated. `bits` and `size` must be
+299
;;; non-negative; passing a negative fixnum signals a type error.
300
;;;
301
;;; ```scheme
302
;;; (mpi-shift-l #u8(0 0 0 1) 3 4) ; => #u8(0 0 0 8)
303
;;; ```
304
(define-native (mpi-shift-l a-bv bits size)
287
(: bytevector? integer? integer? -> bytevector?))
+305
(: bytevector? integer? integer? -> (any-of bytevector? boolean?)))
306
307
;;; Right-shift a bytevectored big integer by `bits` positions.
308
;;;
test/test-crypto.sglmodified
@@ -998,7 +998,14 @@
998
(let ((a #u8(0 0 0 5))
999
(b #u8(0 0 0 3))
1000
(result (mpi-sub a b 4)))
1001
(assert-equal 2 (bytevector-u8-ref result 3)))))
+1001
(assert-equal 2 (bytevector-u8-ref result 3))))
+1002
+1003
(test "3 - 5 yields |3 - 5| = 2 (absolute value, NOT modular wraparound)"
+1004
;; mbedtls_mpi_write_binary writes the magnitude and discards
+1005
;; sign — so 3 - 5 = -2 surfaces as the bytevector for 2, not
+1006
;; as a 2^32 wraparound (which would be #u8(255 255 255 254)).
+1007
(assert-equal #u8(0 0 0 2)
+1008
(mpi-sub #u8(0 0 0 3) #u8(0 0 0 5) 4))))
1009
1010
(test-group "mpi-mul"
1011
(test "3 * 4 = 12 (big-endian, 4 bytes)"
@@ -1087,7 +1094,19 @@
1094
(test-group "mpi-shift-l"
1095
(test "1 << 3 = 8 (big-endian, 4 bytes)"
1096
(let ((result (mpi-shift-l #u8(0 0 0 1) 3 4)))
1090
(assert-equal 8 (bytevector-u8-ref result 3)))))
+1097
(assert-equal 8 (bytevector-u8-ref result 3))))
+1098
+1099
(test "shifted value that does not fit in `size` bytes returns #f"
+1100
;; 1 << 32 = 0x1_0000_0000 needs 5 bytes; size=4 cannot hold it,
+1101
;; so the underlying mbedtls_mpi_write_binary signals
+1102
;; MBEDTLS_ERR_MPI_BUFFER_TOO_SMALL and the function returns #f.
+1103
(assert-false (mpi-shift-l #u8(0 0 0 1) 32 4)))
+1104
+1105
(test "negative bits raises an error"
+1106
(assert-error (mpi-shift-l #u8(0 0 0 1) -1 4)))
+1107
+1108
(test "negative size raises an error"
+1109
(assert-error (mpi-shift-l #u8(0 0 0 1) 1 -1))))
1110
1111
(test-group "mpi-shift-r"
1112
(test "8 >> 3 = 1 (big-endian, 4 bytes)"