Commite0a4e889Recorded23 Sep 2026Repositorysigil-crypto

Act on adversarial review of the Ed25519 and minisign work

Message
  • The minisign parser measures lines as minisign 0.12 does (a trailing CR counted): 1022-byte comment lines, an 8190-byte trusted comment line, no NUL in the untrusted comment, and a positional, bounded public key comment line.
  • Docs: the divergence from libsodium also covers a mixed-order R under an honest key; recommend minisign-verify-prehashed, since an ED signature relabelled Ed verifies over the digest, as with minisign -V.
  • Vectors: fix the mis-extracted Wycheproof tc30; the generator refuses malformed lines and decodes hex outside the guard; add the crafted mixed-order-R vector as a third pinned divergence.
  • The wasm harness checks the build's objects for Mbed TLS, not its log.
  • vendor/MONOCYPHER.md names the right file and states only checked claims.
Changed
 README.md                                |   7 +++++--
 RELEASES.md                              |   2 +-
 src/sigil/crypto/minisign.sgl            | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
 test/differential/ed25519-vs-libsodium.c |  20 ++++++++++++--------
 test/test-ed25519-vectors.sgl            |  14 +++++++++-----
 test/test-ed25519.sgl                    |  18 ++++++++++++++++++
 test/test-minisign.sgl                   |  66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 test/vectors/ed25519-cases.txt           |   3 ++-
 test/vectors/gen-ed25519-vectors.sh      |  16 ++++++++++++----
 test/wasm/run-wasm-test.mjs              |  19 ++++++++++++++++---
 vendor/MONOCYPHER.md                     |  15 ++++++++++-----
 11 files changed, 222 insertions(+), 62 deletions(-)
Diff
README.mdmodified
@@ -54,8 +54,11 @@ Checks the key id, the signature over the file (`ED`, prehashed with
54
BLAKE2b-512, or legacy `Ed`) and the global signature over the trusted
55
comment, as `minisign -V` does. Verification is cofactored (RFC 8032
56
section 5.1.7) and rejects small-order keys and R values; it agrees with
−57
libsodium, which minisign uses, except for keys with a small-order
−58
component, which an honest minisign key never has.
+57
libsodium, which minisign uses, except when the key or R has a
+58
small-order component, which only the key holder can produce and
+59
minisign never does. Use `minisign-verify-prehashed` (`-V -H`) for new
+60
code: without it, a prehashed signature relabelled as legacy verifies
+61
over the file's BLAKE2b digest, as it does in `minisign -V`.
62
63
## WebAssembly
64
RELEASES.mdmodified
@@ -6,7 +6,7 @@ Sigil can now check minisign signatures itself. `(sigil crypto)` gains Ed25519 (
6
7
## Changes
8
−9
- `ed25519-verify` checks an Ed25519 signature and returns `#t` or `#f`. It rejects non-canonical encodings, small-order keys and R values, and S values outside the group order, and checks the cofactored equation that RFC 8032 section 5.1.7 allows. It agrees with libsodium, which the minisign tool uses, on every RFC 8032, Wycheproof and ed25519-speccheck vector except speccheck cases 4 and 5, which need a public key with a small-order component and which an honest key never has.
+9
- `ed25519-verify` checks an Ed25519 signature and returns `#t` or `#f`. It rejects non-canonical encodings, small-order keys and R values, and S values outside the group order, and checks the cofactored equation that RFC 8032 section 5.1.7 allows. It agrees with libsodium, which the minisign tool uses, on every RFC 8032, Wycheproof and ed25519-speccheck vector except where the key or R has a small-order component (speccheck cases 4 and 5, and a crafted signature under an honest key with a mixed-order R); only the key holder can produce those, and minisign never does.
10
- `ed25519-public-key` and `ed25519-sign` derive a key and sign with a 32-byte seed; they come with the vendored implementation.
11
- `blake2b-512` hashes a string or bytevector to 64 bytes.
12
- `(sigil crypto minisign)`: `minisign-parse-public-key` (a `minisign.pub` file or the bare key line), `minisign-parse-signature`, and `minisign-verify` / `minisign-verify-prehashed`, which return the authenticated trusted comment or `#f`. `minisign-failure-reason` returns `#f` for a good signature and otherwise why it failed: `key-id-mismatch`, `legacy-refused`, `bad-signature` or `bad-trusted-comment-signature`. Both minisign formats verify: prehashed `ED` and legacy `Ed`.
src/sigil/crypto/minisign.sglmodified
@@ -12,7 +12,7 @@
12
;;; (define sig (minisign-parse-signature
13
;;; (read-file-bytes "SHA256SUMS.minisig")))
14
;;;
−15
;;; (minisign-verify key sig (read-file-bytes "SHA256SUMS"))
+15
;;; (minisign-verify-prehashed key sig (read-file-bytes "SHA256SUMS"))
16
;;; ; => the trusted comment string when valid, #f otherwise
17
;;; ```
18
;;;
@@ -33,22 +33,38 @@
33
;;; the parsed signature does not expose it, so an unverified comment
34
;;; cannot be read by mistake.
35
;;;
+36
;;; **Use `minisign-verify-prehashed` for anything new.** The algorithm
+37
;;; bytes are not signed, so a prehashed `ED` signature relabelled as
+38
;;; legacy `Ed` verifies under `minisign-verify` as a signature over the
+39
;;; 64-byte BLAKE2b digest of the file. That is also what `minisign -V`
+40
;;; without `-H` accepts; refusing legacy signatures closes it.
+41
;;;
+42
;;; The trusted comment comes back as a string made from its bytes as
+43
;;; signed. If the signer put invalid UTF-8 there, the string carries
+44
;;; those bytes; only the key holder can do that.
+45
;;;
46
;;; Ed25519 here accepts what RFC 8032 section 5.1.7 accepts (cofactored
47
;;; equation, canonical encodings, S < L) and rejects small-order keys and
48
;;; R values as libsodium does. Verification is cofactored, deliberately:
49
;;; RFC 8032 allows it and it agrees with batch verification. It accepts
−40
;;; more than libsodium (which the minisign tool uses) only when the key or
−41
;;; R has a small-order component (ed25519-speccheck cases 4 and 5), which
−42
;;; only the key holder can produce and an honest minisign key never has.
+50
;;; more than libsodium (which the minisign tool uses) only when the key
+51
;;; or R has a small-order component: ed25519-speccheck cases 4 and 5 (a
+52
;;; mixed-order key), and a signature under an honest key whose R carries
+53
;;; a torsion component. Only the key holder can produce either, and
+54
;;; minisign itself never does.
55
;;;
56
;;; ## Parsing is strict
57
;;;
−46
;;; Anything `minisign` would reject is rejected here, and a few things it
−47
;;; would tolerate are rejected too: base64 must be canonical (standard
−48
;;; alphabet, correct padding, zero pad bits), lines may end in LF or CRLF
−49
;;; but may not contain other CR or NUL bytes, and nothing but blank lines
−50
;;; may follow the fourth line of a signature file. Parse errors raise;
−51
;;; a well-formed signature that does not verify is not an error.
+58
;;; Any `.minisig` or `minisign.pub` file that minisign 0.12 rejects is
+59
;;; rejected here, including its line-length limits (1022 bytes for a
+60
;;; comment line, 8190 for the trusted comment line, a trailing CR
+61
;;; counted). A few things it tolerates are rejected too: base64 must be
+62
;;; canonical (standard alphabet, correct padding, zero pad bits), lines
+63
;;; may end in LF or CRLF but the signature and trusted comment lines may
+64
;;; not contain other CR or NUL bytes, and nothing but blank lines may
+65
;;; follow the fourth line of a signature file. The bare key line given
+66
;;; in place of a file may end in a newline. Parse errors raise; a
+67
;;; well-formed signature that does not verify is not an error.
68
;;;
69
;;; ## Every target, including the browser
70
;;;
@@ -99,9 +115,16 @@
115
(define untrusted-prefix "untrusted comment: ")
116
(define trusted-prefix "trusted comment: ")
117
−102
;; minisign reads the trusted comment line with fgets into an 8192-byte
−103
;; buffer (TRUSTEDCOMMENTMAXBYTES) and refuses a line that does not
−104
;; fit with its newline and NUL: 8190 bytes including the prefix.
+118
;; minisign 0.12 reads each comment line with fgets into a fixed buffer
+119
;; and refuses a line whose LF does not fit in it with the terminating
+120
;; NUL (sig_load, minisign.c:170-203; trim, helpers.c:159). So a line,
+121
;; counting a trailing CR but not the LF, may be at most buffer - 2
+122
;; bytes: 1022 for the untrusted comment (COMMENTMAXBYTES 1024) and
+123
;; 8190 for the trusted comment line with its prefix
+124
;; (TRUSTEDCOMMENTMAXBYTES 8192). A public key file's comment line has
+125
;; the same 1024-byte buffer; a longer one makes minisign read the rest
+126
;; of it as the key.
+127
(define max-comment-line-bytes 1022)
128
(define max-trusted-line-bytes 8190)
129
130
(define (fail what)
@@ -136,18 +159,24 @@
159
(or (= (bytevector-u8-ref bv i) byte)
160
(loop (+ i 1))))))
161
−139
;; Split on LF. Each line loses one trailing CR. The piece after a
−140
;; final LF is an empty line, which callers treat as blank.
−141
(define (split-lines bv)
+162
;; Split on LF, keeping each line's bytes as they are (a trailing CR
+163
;; included), because minisign's length limits count that CR. The
+164
;; piece after a final LF is an empty line, which callers treat as
+165
;; blank. Use strip-cr for a line's content.
+166
(define (split-raw-lines bv)
167
(let ((n (bytevector-length bv)))
168
(let loop ((i 0) (start 0) (acc '()))
169
(cond
170
((= i n)
−146
(reverse (cons (strip-cr (bv-slice bv start n)) acc)))
+171
(reverse (cons (bv-slice bv start n) acc)))
172
((= (bytevector-u8-ref bv i) 10)
−148
(loop (+ i 1) (+ i 1) (cons (strip-cr (bv-slice bv start i)) acc)))
+173
(loop (+ i 1) (+ i 1) (cons (bv-slice bv start i) acc)))
174
(else (loop (+ i 1) start acc))))))
175
+176
(define (check-line-length raw-line limit what)
+177
(when (> (bytevector-length raw-line) limit)
+178
(fail (string-append what " is too long"))))
+179
180
(define (strip-cr line)
181
(let ((n (bytevector-length line)))
182
(if (and (> n 0) (= (bytevector-u8-ref line (- n 1)) 13))
@@ -221,19 +250,23 @@
250
;;; Parse a minisign public key.
251
;;;
252
;;; `text` (string or bytevector) is either the contents of a
−224
;;; `minisign.pub` file (an `untrusted comment:` line, then the key line)
−225
;;; or the bare base64 key line, as given to `minisign -P`. Raises on
−226
;;; anything else.
+253
;;; `minisign.pub` file (an `untrusted comment:` line first, then the
+254
;;; key line) or the bare base64 key line, as given to `minisign -P`.
+255
;;; Only blank lines may follow the key. Raises on anything else.
256
(define (minisign-parse-public-key text)
−228
(let* ((lines (split-lines (->bytes text "public key")))
−229
(content (filter-non-blank lines))
+257
(let* ((raw-lines (split-raw-lines (->bytes text "public key")))
+258
(lines (map strip-cr raw-lines))
+259
(commented? (bv-prefix? (car lines) "untrusted comment:"))
+260
(key-lines (if commented? (cdr lines) lines))
261
(key-line
−231
(cond
−232
((and (= (length content) 2)
−233
(bv-prefix? (car content) "untrusted comment:"))
−234
(cadr content))
−235
((= (length content) 1) (car content))
−236
(else (fail "public key must be one base64 line, optionally after an untrusted comment line")))))
+262
(if (and (pair? key-lines)
+263
(not (blank? (car key-lines)))
+264
(null? (filter-non-blank (cdr key-lines))))
+265
(car key-lines)
+266
(fail "public key must be one base64 line, optionally after an untrusted comment line"))))
+267
(when commented?
+268
(check-line-length (car raw-lines) max-comment-line-bytes
+269
"public key comment line"))
270
(check-line-bytes key-line "public key")
271
(let ((raw (base64-decode-exact key-line 42 "public key")))
272
(unless (and (= (bytevector-u8-ref raw 0) 69) ; E
@@ -274,7 +307,8 @@
307
;;; comment: ...`, the base64 global signature. Raises when the file is
308
;;; malformed or uses an algorithm other than `ED` or `Ed`.
309
(define (minisign-parse-signature text)
−277
(let* ((lines (split-lines (->bytes text "signature")))
+310
(let* ((raw-lines (split-raw-lines (->bytes text "signature")))
+311
(lines (map strip-cr raw-lines))
312
(n (length lines)))
313
(when (< n 4)
314
(fail "signature file has fewer than four lines"))
@@ -288,8 +322,14 @@
322
(fail "first line must start with \"untrusted comment: \""))
323
(unless (bv-prefix? trusted-line trusted-prefix)
324
(fail "third line must start with \"trusted comment: \""))
−291
(when (> (bytevector-length trusted-line) max-trusted-line-bytes)
−292
(fail "trusted comment is too long"))
+325
(check-line-length (nth raw-lines 0) max-comment-line-bytes
+326
"untrusted comment")
+327
(check-line-length (nth raw-lines 2) max-trusted-line-bytes
+328
"trusted comment")
+329
;; minisign's strlen stops at a NUL before the LF, so it refuses
+330
;; the line; a CR in the untrusted comment is harmless to it.
+331
(when (bv-contains-byte? untrusted 0)
+332
(fail "untrusted comment contains a NUL byte"))
333
(check-line-bytes sig-line "signature")
334
(check-line-bytes trusted-line "trusted comment")
335
(check-line-bytes global-line "global signature")
test/differential/ed25519-vs-libsodium.cmodified
@@ -10,8 +10,9 @@
10
*
11
* Output ends with a summary line
12
* SUMMARY cases=N agree=A expected-divergence=E disagree=D mutants=M ...
−13
* and the exit status is 0 only when N > 0, D = 0, E = 2 (speccheck 4 and
−14
* 5, see below), no mutant is accepted or disagrees, all 200 unmutated
+13
* and the exit status is 0 only when N > 0, D = 0, E = 3 (speccheck 4 and
+14
* 5 and crafted-mixed-order-r, see below), no mutant is accepted or
+15
* disagrees, all 200 unmutated
16
* signatures verify, and all 20 units (7 encoding boundaries, 10 small-order
17
* encodings, 3 large-order controls) pass.
18
*
@@ -87,12 +88,15 @@ int main(void)
88
int ours = sigil_crypto_ed25519_verify(pk, msg, (size_t)mlen, sig);
89
int theirs = sodium_verdict(pk, sig, msg, (size_t)mlen);
90
cases++;
−90
/* The documented divergence: speccheck cases 4 and 5 need a public
−91
* key with a small-order component and pass the cofactored
−92
* equation (ours) but not libsodium's cofactorless one. They must
−93
* diverge in exactly that direction; anything else is a finding. */
+91
/* The documented divergence, cofactored (ours) against cofactorless
+92
* (libsodium): speccheck cases 4 and 5 need a public key with a
+93
* small-order component; crafted-mixed-order-r is an honest key with
+94
* R = rB + T8. Only the key holder can produce either. They must
+95
* diverge in exactly this direction (ours accepts, libsodium
+96
* rejects); anything else is a finding. */
97
int expected_divergence = strcmp(label, "speccheck-4") == 0 ||
−95
strcmp(label, "speccheck-5") == 0;
+98
strcmp(label, "speccheck-5") == 0 ||
+99
strcmp(label, "crafted-mixed-order-r") == 0;
100
const char *tag;
101
if (expected_divergence) {
102
if (ours == 1 && theirs == 0) {
@@ -226,7 +230,7 @@ int main(void)
230
"mutant-disagree=%ld valid-ok=%ld unit=%ld unit-bad=%ld\n",
231
cases, agree, expected_diverged, disagree, mutants, mutant_disagree, valid_ok,
232
unit, unit_bad);
−229
return (cases > 0 && disagree == 0 && expected_diverged == 2 &&
+233
return (cases > 0 && disagree == 0 && expected_diverged == 3 &&
234
mutant_disagree == 0 &&
235
valid_ok == 200 && unit == 20 && unit_bad == 0) ? 0 : 1;
236
}
test/test-ed25519-vectors.sglmodified
@@ -20,9 +20,11 @@
20
bv)))))
21
22
;; #t, #f, or the symbol raised when the call raises.
+23
;; Hex is decoded outside the guard: only ed25519-verify may raise.
24
(define (verdict pk sig msg)
−24
(guard (e (#t (quote raised)))
−25
(ed25519-verify (hex->bv pk) (hex->bv msg) (hex->bv sig))))
+25
(let ((pk (hex->bv pk)) (sig (hex->bv sig)) (msg (hex->bv msg)))
+26
(guard (e (#t (quote raised)))
+27
(ed25519-verify pk msg sig))))
28
29
(test-group "ed25519 vectors"
30
(test "rfc8032-1 valid"
@@ -93,8 +95,8 @@
95
(assert-eq #f (verdict "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fedd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" "3f")))
96
(test "wycheproof-29 invalid"
97
(assert-eq #f (verdict "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fedffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" "3f")))
−96
(test "wycheproof-30 "
−97
(assert-eq (quote raised) (verdict "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" "54657374" "invalid")))
+98
(test "wycheproof-30 invalid"
+99
(assert-eq (quote raised) (verdict "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" "-" "54657374")))
100
(test "wycheproof-31 invalid"
101
(assert-eq (quote raised) (verdict "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" "7c38e026f29e14aabd059a0f2db8b0cd783040609a8be684db12f82a27774ab0" "54657374")))
102
(test "wycheproof-32 invalid"
@@ -361,6 +363,8 @@
363
(assert-eq #f (verdict "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04" "e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b")))
364
(test "speccheck-11 invalid"
365
(assert-eq #f (verdict "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04" "39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f")))
+366
(test "crafted-mixed-order-r valid"
+367
(assert-eq #t (verdict "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8" "12b8d902558f234e020abb7be4e3bd2c2f1e847c3e49560413512eecab8f131ec05dc8d713151494f13f8bace8263ae68b6e564f07764b39bd1f9d25b1bf6f08" "6d697865642d6f7264657220522c20686f6e657374206b6579")))
368
)
369
−366
;;; 168 cases
+370
;;; 169 cases
test/test-ed25519.sglmodified
@@ -131,6 +131,24 @@
131
(assert-eq #f (ed25519-verify identity "anything" forged))
132
(assert-eq #f (ed25519-verify identity "anything else" forged)))))
133
+134
;; The second shape of the documented divergence from libsodium, crafted by
+135
;; the adversarial review (independent Python curve arithmetic): an honest
+136
;; key from seed 00..1f, and R = rB + T8 with T8 of order 8. Cofactored
+137
;; verification accepts it; libsodium's cofactorless check rejects it
+138
;; (test/differential/run.sh pins that). Only the key holder can make one.
+139
(test-group "ed25519 verify: documented cofactored acceptance"
+140
(test "the crafted key is the honest key for its seed"
+141
(let ((seed (let ((b (make-bytevector 32 0)))
+142
(let loop ((i 0))
+143
(if (< i 32) (begin (bytevector-u8-set! b i i) (loop (+ i 1))) b)))))
+144
(assert-equal "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8"
+145
(bv->hex (ed25519-public-key seed)))))
+146
(test "a mixed-order R under that key is accepted"
+147
(assert-eq #t (ed25519-verify
+148
(hex->bv "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8")
+149
(hex->bv "6d697865642d6f7264657220522c20686f6e657374206b6579")
+150
(hex->bv "12b8d902558f234e020abb7be4e3bd2c2f1e847c3e49560413512eecab8f131ec05dc8d713151494f13f8bace8263ae68b6e564f07764b39bd1f9d25b1bf6f08")))))
+151
152
(test-group "ed25519 verify: argument checking"
153
(test "a 31-byte public key raises"
154
(assert-error (ed25519-verify (make-bytevector 31 0) "m" t1-sig)))
test/test-minisign.sglmodified
@@ -286,16 +286,80 @@
286
(assert-false (minisign-verify-prehashed key-a (sig "hello.txt.Ed.minisig") (fixture "hello.txt"))))
287
(test "a legacy signature relabelled as prehashed fails"
288
;; Changing Ed to ED makes the verifier hash first, so the same
−289
;; Ed25519 signature no longer matches: the algorithm is bound.
+289
;; Ed25519 signature no longer matches the file. (The algorithm bytes
+290
;; are not signed; the other direction is the next test.)
291
(let* ((ls (lines-of "hello.txt.Ed.minisig"))
292
(relabelled (resign-line (cadr ls) 0 (bytevector 69 68))))
293
(assert-eq 'bad-signature
294
(minisign-failure-reason key-a
295
(minisign-parse-signature (join-lines (with-line ls 1 relabelled)))
296
(fixture "hello.txt") #f))))
+297
(test "a prehashed signature relabelled as legacy: -V accepts the digest, -V -H refuses"
+298
;; Documented behaviour, shared with `minisign -V`: relabelled ED -> Ed
+299
;; is a valid legacy signature over the 64-byte BLAKE2b digest of the
+300
;; file. minisign-verify-prehashed is the defence.
+301
(let* ((ls (lines-of "hello.txt.ED.minisig"))
+302
(relabelled (minisign-parse-signature
+303
(join-lines (with-line ls 1 (resign-line (cadr ls) 0 (bytevector 69 100))))))
+304
(digest (blake2b-512 (fixture "hello.txt"))))
+305
(assert-equal "sigil-crypto fixture: prehashed" (minisign-verify key-a relabelled digest))
+306
(assert-false (minisign-verify key-a relabelled (fixture "hello.txt")))
+307
(assert-false (minisign-verify-prehashed key-a relabelled digest))
+308
(assert-eq 'legacy-refused (minisign-failure-reason key-a relabelled digest #t))))
309
(test "minisign-verify returns #f, not a comment, on failure"
310
(assert-false (minisign-verify key-a (sig "hello.txt.ED.minisig") "tampered"))))
311
+312
;; minisign 0.12's line limits (sig_load, minisign.c:170-203): a line,
+313
;; counting a trailing CR but not the LF, may be 1022 bytes for a comment
+314
;; and 8190 for the trusted comment line. Each limit is tested at the
+315
;; boundary, both sides, with and without CR.
+316
(define (x-line prefix total)
+317
(string-append prefix (make-string (- total (string-length prefix)) #\x)))
+318
+319
(define (sig-with-line index new)
+320
(join-lines (with-line (lines-of "hello.txt.ED.minisig") index new)))
+321
+322
(test-group "minisign line limits match minisign 0.12"
+323
(test "an untrusted comment line of 1022 bytes parses"
+324
(assert-true (minisign-signature? (minisign-parse-signature
+325
(sig-with-line 0 (x-line "untrusted comment: " 1022))))))
+326
(test "an untrusted comment line of 1023 bytes raises"
+327
(assert-true (raises-with? "untrusted comment is too long"
+328
(lambda () (minisign-parse-signature
+329
(sig-with-line 0 (x-line "untrusted comment: " 1023)))))))
+330
(test "1021 bytes plus CR parses; 1022 bytes plus CR raises"
+331
(assert-true (minisign-signature? (minisign-parse-signature
+332
(sig-with-line 0 (string-append (x-line "untrusted comment: " 1021) (string #\return))))))
+333
(assert-true (raises-with? "untrusted comment is too long"
+334
(lambda () (minisign-parse-signature
+335
(sig-with-line 0 (string-append (x-line "untrusted comment: " 1022) (string #\return))))))))
+336
(test "a NUL in the untrusted comment raises"
+337
(assert-true (raises-with? "untrusted comment contains a NUL"
+338
(lambda () (minisign-parse-signature
+339
(sig-with-line 0 (string-append "untrusted comment: a" (string (integer->char 0)) "b")))))))
+340
(test "a trusted comment line of 8190 bytes parses, 8191 raises"
+341
(assert-true (minisign-signature? (minisign-parse-signature
+342
(sig-with-line 2 (x-line "trusted comment: " 8190)))))
+343
(assert-true (raises-with? "trusted comment is too long"
+344
(lambda () (minisign-parse-signature
+345
(sig-with-line 2 (x-line "trusted comment: " 8191)))))))
+346
(test "a trusted comment line of 8190 bytes plus CR raises"
+347
(assert-true (raises-with? "trusted comment is too long"
+348
(lambda () (minisign-parse-signature
+349
(sig-with-line 2 (string-append (x-line "trusted comment: " 8190) (string #\return))))))))
+350
(test "a public key comment line of 1022 bytes parses, 1023 raises"
+351
(let ((key-line (cadr (lines-of "a.pub"))))
+352
(assert-true (minisign-public-key? (minisign-parse-public-key
+353
(string-append (x-line "untrusted comment: " 1022) "\n" key-line "\n"))))
+354
(assert-true (raises-with? "public key comment line is too long"
+355
(lambda () (minisign-parse-public-key
+356
(string-append (x-line "untrusted comment: " 1023) "\n" key-line "\n")))))))
+357
(test "a blank line before a public key file's comment raises"
+358
;; minisign reads the first line as the comment, whatever it is.
+359
(assert-true (raises-with? "must be one base64 line"
+360
(lambda () (minisign-parse-public-key
+361
(string-append "\n" (fixture-text "a.pub"))))))))
+362
363
(test-group "minisign argument checking"
364
(test "minisign-failure-reason refuses an unparsed key"
365
(assert-true (raises-with? "not a parsed public key" (lambda () (minisign-failure-reason root-key-line (sig "hello.txt.ED.minisig") "x" #f)))))
test/vectors/ed25519-cases.txtmodified
@@ -32,7 +32,7 @@ wycheproof-26 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa e
32
wycheproof-27 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fecd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010 3f invalid
33
wycheproof-28 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fedd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010 3f invalid
34
wycheproof-29 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fedffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f 3f invalid
−35
wycheproof-30 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa 54657374 invalid
+35
wycheproof-30 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa - 54657374 invalid
36
wycheproof-31 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa 7c38e026f29e14aabd059a0f2db8b0cd783040609a8be684db12f82a27774ab0 54657374 invalid
37
wycheproof-32 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa 7c38e026f29e14aabd059a0f2db8b0cd783040609a8be684db12f82a27774ab07a9155711ecfaf7f99f277bad0c6ae7e39d4eef676573336a5c51eb6f946 54657374 invalid
38
wycheproof-33 7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa 7c38e026f29e14aabd059a0f2db8b0cd783040609a8be684db12f82a27774ab07a9155711ecfaf7f99f277bad0c6ae7e39d4eef676573336a5c51eb6f946b30d2020 54657374 invalid
@@ -166,3 +166,4 @@ speccheck-8 f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43 ecf
166
speccheck-9 f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43 ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffca8c5b64cd208982aa38d4936621a4775aa233aa0505711d8fdcfdaa943d4908 9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41 ?
167
speccheck-10 ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04 e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b ?
168
speccheck-11 ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04 39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f ?
+169
crafted-mixed-order-r 03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8 12b8d902558f234e020abb7be4e3bd2c2f1e847c3e49560413512eecab8f131ec05dc8d713151494f13f8bace8263ae68b6e564f07764b39bd1f9d25b1bf6f08 6d697865642d6f7264657220522c20686f6e657374206b6579 valid
test/vectors/gen-ed25519-vectors.shmodified
@@ -5,6 +5,10 @@
5
# rfc8032-* RFC 8032 section 7.1 (Ed25519), expected valid
6
# wycheproof-* C2SP/wycheproof testvectors_v1/ed25519_test.json
7
# (commit 5722833ca004983abd1a91bcb6c24596d50ac0f9)
+8
# crafted-* crafted by the adversarial review: crafted-mixed-order-r is an
+9
# honest key (seed 00..1f) and R = rB + T8 (T8 of order 8), which
+10
# passes the cofactored equation and fails libsodium's cofactorless
+11
# one: the second shape of the documented divergence.
12
# speccheck-* novifinancial/ed25519-speccheck cases.json
13
# (commit 5e4bfc4542293286e9ad3cb2b805badee00503de), whose
14
# expected column is "?": filled in below.
@@ -45,12 +49,15 @@ BEGIN {
49
print " bv)))))"
50
print ""
51
print ";; #t, #f, or the symbol raised when the call raises."
+52
print ";; Hex is decoded outside the guard: only ed25519-verify may raise."
53
print "(define (verdict pk sig msg)"
−49
print " (guard (e (#t (quote raised)))"
−50
print " (ed25519-verify (hex->bv pk) (hex->bv msg) (hex->bv sig))))"
+54
print " (let ((pk (hex->bv pk)) (sig (hex->bv sig)) (msg (hex->bv msg)))"
+55
print " (guard (e (#t (quote raised)))"
+56
print " (ed25519-verify pk msg sig))))"
57
print ""
58
print "(test-group \"ed25519 vectors\""
59
}
+60
NF != 5 { printf "REFUSING: line %d has %d fields, not 5: %s\n", NR, NF, $1 > "/dev/stderr"; bad = 1; exit 1 }
61
{
62
label=$1; pk=$2; sig=$3; msg=$4; want_result=$5
63
if (label ~ /^speccheck-/) { k=label; sub(/^speccheck-/, "", k); want_result=spec[k] }
@@ -60,6 +67,7 @@ BEGIN {
67
printf " (test \"%s %s\"\n %s)\n", label, want_result, want
68
n++
69
}
−63
END { print " )"; print ""; printf ";;; %d cases\n", n }
−64
' "$DIR/ed25519-cases.txt" > "$OUT"
+70
END { if (bad) exit 1; print " )"; print ""; printf ";;; %d cases\n", n }
+71
' "$DIR/ed25519-cases.txt" > "$OUT.tmp"
+72
mv "$OUT.tmp" "$OUT"
73
echo "wrote $OUT"
test/wasm/run-wasm-test.mjsmodified
@@ -99,9 +99,21 @@ function build() {
99
process.stdout.write(web.out);
100
setupFailed(`web build failed (exit ${web.status})`);
101
}
−102
if (/vendor\/mbedtls\/library/.test(web.out)) {
−103
fail("the web build compiled Mbed TLS sources; the wasm build must skip them");
−104
}
+102
}
+103
+104
// The wasm build's objects, not its log (a cached build logs nothing): no
+105
// Mbed TLS object may exist, and Monocypher's must, which is the positive
+106
// control that this looks in the right place.
+107
function checkWasmObjects() {
+108
const objDir = path.join(probeDir, "build/web/native");
+109
const monocypherObj = path.join(objDir, "vendor/monocypher/src/monocypher.o");
+110
if (!fs.existsSync(monocypherObj)) setupFailed(`expected wasm object missing: ${monocypherObj}`);
+111
const mbedtlsDir = path.join(objDir, "vendor/mbedtls");
+112
const found = fs.existsSync(mbedtlsDir)
+113
? fs.readdirSync(mbedtlsDir, { recursive: true }).filter((f) => String(f).endsWith(".o"))
+114
: [];
+115
if (found.length > 0) fail(`the wasm build has ${found.length} Mbed TLS objects (e.g. ${found[0]})`);
+116
else console.log("wasm objects: Monocypher present, no Mbed TLS objects");
117
}
118
119
function describe(file) {
@@ -268,6 +280,7 @@ async function main() {
280
describe(hostBin);
281
describe(wasmPath);
282
describe(staticBin);
+283
checkWasmObjects();
284
285
// Host run. cwd is the probe dir, for the same reason as the build.
286
const host = run(hostBin, [], { cwd: probeDir });
vendor/MONOCYPHER.mdmodified
@@ -42,15 +42,20 @@ signatures. Mbed TLS 3.6 has neither EdDSA nor BLAKE2b.
42
signing. Verification only handles public data, but the signing entry
43
point is exported too.
44
−45
Alternatives considered: libsodium (what minisign itself uses, ISC, but
−46
tens of thousands of lines and a platform layer), TweetNaCl (public domain,
−47
smallest, but slow and not constant-time in places), and the SUPERCOP
−48
ref10 code (public domain, unmaintained as a library).
+45
Alternatives considered: libsodium (what minisign itself uses, ISC; a much
+46
larger library with its own platform and entropy layer, which the wasm build
+47
would have to carry), TweetNaCl (public domain and smallest, but its
+48
verification skips the S < L check and it is not maintained as a library),
+49
and the SUPERCOP ref10 code (public domain, not packaged as a library).
+50
Monocypher 3.1.3 was reconsidered during review because its verification is
+51
cofactorless like libsodium's; it was not taken because 3.x lacks 4.0.3's
+52
signing timing fix (topics/monocypher-cofactored-verification-vs-libsodium
+53
in the estate notes).
54
55
## Verification rules
56
57
`crypto_ed25519_check` checks the cofactored equation `[8](sB - hA - R) = 0`,
58
rejects `S >= L`, and accepts non-canonical encodings of A and R. sigil-crypto's
−54
wrapper (`native/ed25519.c`) additionally rejects non-canonical encodings of A
+59
wrapper (`native/ed25519-verify.c`) additionally rejects non-canonical encodings of A
60
and R (RFC 8032 section 5.1.3) and small-order A and R (as libsodium does), so
61
the accept set is RFC 8032 section 5.1.7 plus libsodium's small-order rule.