AtlatestRepositorysigil-crypto
sigil-crypto / tree / testtest-minisign.sgl
1
;;; (sigil crypto minisign) against signatures made by the real minisign.2
;;;3
;;; Fixtures live in test/fixtures/minisign and were made by4
;;; make-fixtures.sh with minisign 0.12, which also checked every one with5
;;; `minisign -V` before keeping it. Two come from other people's keys:6
;;; production registry.json under the Sigil registry root key, and the7
;;; minisign 0.12 source tarball under the minisign author's key.8
;;;9
;;; Every negative asserts the reason `minisign-failure-reason` gives, so a test10
;;; cannot pass because verification failed for some other cause.12
(import (sigil test)13
(sigil crypto)14
(sigil crypto minisign)15
(sigil fs)16
(sigil io)17
(sigil math)18
(sigil string))20
(define fixture-dir "test/fixtures/minisign/")22
(define (fixture name) (read-file-bytes (string-append fixture-dir name)))23
(define (fixture-text name) (utf8->string (fixture name)))24
(define (nth lst k) (if (= k 0) (car lst) (nth (cdr lst) (- k 1))))26
(define root-key-line "RWRa9dPSUBFexBbLdzZIfuAmuCYL736UeHC7IbdAOIYpgGIyDmWRaQHY")27
(define minisign-author-key-line "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")29
(define key-a (minisign-parse-public-key (fixture "a.pub")))30
(define key-b (minisign-parse-public-key (fixture "b.pub")))31
(define root-key (minisign-parse-public-key root-key-line))32
(define author-key (minisign-parse-public-key minisign-author-key-line))34
(define (sig name) (minisign-parse-signature (fixture name)))36
;; The lines of a text fixture, and a way to rebuild it with one changed.37
(define (lines-of name) (string-split (fixture-text name) "\n"))38
(define (join-lines lines) (string-join lines "\n"))39
(define (with-line lines index new)40
(let loop ((i 0) (ls lines) (acc '()))41
(if (null? ls)42
(reverse acc)43
(loop (+ i 1) (cdr ls) (cons (if (= i index) new (car ls)) acc)))))45
;; Replace the character at `index` of `s` with `c`.46
(define (string-with-char s index c)47
(string-append (substring s 0 index) (string c)48
(substring s (+ index 1) (string-length s))))50
;; A different base64 alphabet character than `c`.51
(define (other-b64-char c) (if (char=? c #\A) #\B #\A))53
(define (flip-byte bv index)54
(let ((out (bytevector-copy bv)))55
(bytevector-u8-set! out index (bitwise-xor (bytevector-u8-ref out index) 1))56
out))58
;; Rebuild a signature file's second line with bytes [start, end) of the59
;; decoded signature replaced by `new-bytes`.60
(define (resign-line line start new-bytes)61
(let* ((raw (base64-decode line))62
(out (bytevector-copy raw)))63
(let loop ((i 0))64
(when (< i (bytevector-length new-bytes))65
(bytevector-u8-set! out (+ start i) (bytevector-u8-ref new-bytes i))66
(loop (+ i 1))))67
(base64-encode out)))69
;; #t when thunk raises an error whose message contains `needle`.70
(define (raises-with? needle thunk)71
(guard (e (#t (and (error-object? e)72
(string-contains? (error-object-message e) needle))))73
(thunk)74
#f))76
(define (raises? thunk)77
(guard (e (#t #t)) (thunk) #f))79
(test-group "minisign fixtures are present"80
;; Presence probe with a positive answer: an empty or missing fixture81
;; directory must fail here, not quietly weaken the tests below.82
(test "hello.txt reads back"83
(assert-equal "hello, minisign\n" (fixture-text "hello.txt")))84
(test "binary.bin is all 1024 bytes"85
(assert-equal 1024 (bytevector-length (fixture "binary.bin")))))87
(test-group "minisign public keys"88
(test "a .pub file parses and its id matches the file's comment"89
(let* ((first-line (car (lines-of "a.pub")))90
(id (substring first-line (- (string-length first-line) 16)91
(string-length first-line))))92
(assert-equal id (minisign-public-key-id key-a))))93
(test "the bare base64 line parses to the same key"94
(let ((bare (minisign-parse-public-key (cadr (lines-of "a.pub")))))95
(assert-equal (minisign-public-key-id key-a) (minisign-public-key-id bare))96
(assert-equal (minisign-public-key-bytes key-a) (minisign-public-key-bytes bare))))97
(test "the registry root key has the id registry.json lists"98
(assert-equal "C45E1150D2D3F55A" (minisign-public-key-id root-key)))99
(test "public key bytes are 32 bytes"100
(assert-equal 32 (bytevector-length (minisign-public-key-bytes key-a))))101
(test "a key line one character short raises (length)"102
(assert-true (raises-with? "wrong base64 length" (lambda () (minisign-parse-public-key103
(substring root-key-line 0 55))))))104
(test "a key line with a character outside base64 raises"105
(assert-true (raises-with? "character outside base64" (lambda () (minisign-parse-public-key106
(string-with-char root-key-line 20 #\!))))))107
(test "a key whose algorithm is not Ed raises"108
;; "RW" starts the base64 of "Ed"; "RX" makes the algorithm "Et".109
(assert-true (raises-with? "algorithm is not Ed" (lambda () (minisign-parse-public-key110
(string-with-char root-key-line 1 #\X))))))111
(test "three non-blank lines raise"112
(assert-true (raises-with? "must be one base64 line" (lambda () (minisign-parse-public-key113
(string-append (fixture-text "a.pub")114
root-key-line "\n"))))))115
(test "an empty string raises"116
(assert-true (raises-with? "must be one base64 line" (lambda () (minisign-parse-public-key ""))))))118
(test-group "minisign signature parsing"119
(test "ED is prehashed"120
(assert-eq 'prehashed (minisign-signature-algorithm (sig "hello.txt.ED.minisig"))))121
(test "Ed is legacy"122
(assert-eq 'legacy (minisign-signature-algorithm (sig "hello.txt.Ed.minisig"))))123
(test "the signature names key a"124
(assert-equal (minisign-public-key-id key-a)125
(minisign-signature-key-id (sig "hello.txt.ED.minisig"))))126
(test "fewer than four lines raise"127
(let ((ls (lines-of "hello.txt.ED.minisig")))128
(assert-true (raises-with? "fewer than four lines" (lambda () (minisign-parse-signature129
(join-lines (list (car ls) (cadr ls) (caddr ls)))))))))130
(test "a first line without the untrusted prefix raises"131
(let ((ls (lines-of "hello.txt.ED.minisig")))132
(assert-true (raises-with? "first line must start with" (lambda () (minisign-parse-signature133
(join-lines (with-line ls 0 "comment: x"))))))))134
(test "a third line without the trusted prefix raises"135
(let ((ls (lines-of "hello.txt.ED.minisig")))136
(assert-true (raises-with? "third line must start with" (lambda () (minisign-parse-signature137
(join-lines (with-line ls 2 "trusted: x"))))))))138
(test "content after the global signature raises"139
(assert-true (raises-with? "content after the global signature" (lambda () (minisign-parse-signature140
(string-append (fixture-text "hello.txt.ED.minisig")141
"extra\n"))))))142
(test "a CR inside the trusted comment raises"143
(let ((ls (lines-of "hello.txt.ED.minisig")))144
(assert-true (raises-with? "trusted comment contains a CR or NUL" (lambda () (minisign-parse-signature145
(join-lines (with-line ls 2 (string-append "trusted comment: a" (string #\return) "b")))))))))146
(test "an unsupported algorithm raises"147
;; "RU" starts the base64 of "ED"; "RV" makes148
;; the second byte T, so the algorithm reads "ET".149
(let* ((ls (lines-of "hello.txt.ED.minisig"))150
(line (cadr ls)))151
(assert-true (raises-with? "unsupported signature algorithm" (lambda () (minisign-parse-signature152
(join-lines (with-line ls 1 (string-with-char line 1 #\V)))))))))153
(test "non-zero base64 pad bits raise"154
;; The 74-byte signature ends in one '='; its last data character155
;; carries 2 pad bits. Setting them keeps the alphabet valid.156
(let* ((ls (lines-of "hello.txt.ED.minisig"))157
(line (cadr ls))158
(last-data (string-ref line 98))159
(bumped (integer->char (+ 1 (char->integer last-data)))))160
(assert-true (raises-with? "non-canonical base64 padding" (lambda () (minisign-parse-signature161
(join-lines (with-line ls 1 (string-with-char line 98 bumped))))))))))163
(test-group "minisign verification: signatures made by minisign"164
(test "prehashed ED over a text file"165
(assert-equal "sigil-crypto fixture: prehashed"166
(minisign-verify key-a (sig "hello.txt.ED.minisig") (fixture "hello.txt"))))167
(test "legacy Ed over a text file"168
(assert-equal "sigil-crypto fixture: legacy"169
(minisign-verify key-a (sig "hello.txt.Ed.minisig") (fixture "hello.txt"))))170
(test "prehashed ED over an empty file"171
(assert-equal "sigil-crypto fixture: empty file"172
(minisign-verify key-a (sig "empty.txt.ED.minisig") (fixture "empty.txt"))))173
(test "prehashed ED over every byte value"174
(assert-equal "sigil-crypto fixture: binary"175
(minisign-verify key-a (sig "binary.bin.ED.minisig") (fixture "binary.bin"))))176
(test "legacy Ed over every byte value"177
(assert-equal "sigil-crypto fixture: binary legacy"178
(minisign-verify key-a (sig "binary.bin.Ed.minisig") (fixture "binary.bin"))))179
(test "a UTF-8 trusted comment comes back intact"180
(assert-equal "fixture ✓ unicode — trusted"181
(minisign-verify key-a (sig "hello.txt.unicode.minisig") (fixture "hello.txt"))))182
(test "a string message verifies like its bytes"183
(assert-equal "sigil-crypto fixture: prehashed"184
(minisign-verify key-a (sig "hello.txt.ED.minisig") "hello, minisign\n")))185
(test "a signature file with CRLF line endings verifies"186
(let ((crlf (string-join (lines-of "hello.txt.ED.minisig")187
(string #\return #\newline))))188
(assert-equal "sigil-crypto fixture: prehashed"189
(minisign-verify key-a (minisign-parse-signature crlf) (fixture "hello.txt")))))190
(test "minisign-verify-prehashed accepts ED"191
(assert-equal "sigil-crypto fixture: prehashed"192
(minisign-verify-prehashed key-a (sig "hello.txt.ED.minisig") (fixture "hello.txt")))))194
(test-group "minisign verification: real published signatures"195
(test "production registry.json under the registry root key"196
(assert-equal "registry=pkg.usesigil.org path=/v1/meta/registry.json seq=1 ts=2026-07-30T08:04:54Z"197
(minisign-verify-prehashed root-key (sig "registry.json.minisig")198
(fixture "registry.json"))))199
(test "minisign 0.12's source tarball under the minisign author's key"200
(assert-true (string? (minisign-verify author-key (sig "minisign-0.12.tar.gz.minisig")201
(fixture "minisign-0.12.tar.gz")))))202
(test "a one-bit change to registry.json is a bad signature"203
(assert-eq 'bad-signature204
(minisign-failure-reason root-key (sig "registry.json.minisig")205
(flip-byte (fixture "registry.json") 100) #t)))206
(test "a one-bit change to the tarball is a bad signature"207
(assert-eq 'bad-signature208
(minisign-failure-reason author-key (sig "minisign-0.12.tar.gz.minisig")209
(flip-byte (fixture "minisign-0.12.tar.gz") 10000) #f)))210
(test "registry.json.minisig does not verify under a fixture key"211
(assert-eq 'key-id-mismatch212
(minisign-failure-reason key-a (sig "registry.json.minisig") (fixture "registry.json") #f))))214
(test-group "minisign verification: negatives, each with its reason"215
(test "valid control: no failure reason"216
(assert-eq #f (minisign-failure-reason key-a (sig "hello.txt.ED.minisig") (fixture "hello.txt") #f)))217
(test "tampered message, prehashed"218
(assert-eq 'bad-signature219
(minisign-failure-reason key-a (sig "hello.txt.ED.minisig") "hello, minisigN\n" #f)))220
(test "tampered message, legacy"221
(assert-eq 'bad-signature222
(minisign-failure-reason key-a (sig "hello.txt.Ed.minisig") "hello, minisigN\n" #f)))223
(test "tampered binary message, every 64th byte"224
(let loop ((i 0))225
(when (< i 1024)226
(assert-eq 'bad-signature227
(minisign-failure-reason key-a (sig "binary.bin.ED.minisig")228
(flip-byte (fixture "binary.bin") i) #f))229
(loop (+ i 64)))))230
(test "a message with one extra byte"231
(assert-eq 'bad-signature232
(minisign-failure-reason key-a (sig "hello.txt.ED.minisig") "hello, minisign\n\n" #f)))233
(test "the right signature for a different file"234
(assert-eq 'bad-signature235
(minisign-failure-reason key-a (sig "empty.txt.ED.minisig") (fixture "hello.txt") #f)))236
(test "tampered trusted comment"237
(let ((ls (lines-of "hello.txt.ED.minisig")))238
(assert-eq 'bad-trusted-comment-signature239
(minisign-failure-reason key-a240
(minisign-parse-signature241
(join-lines (with-line ls 2 "trusted comment: sigil-crypto fixture: prehashed!")))242
(fixture "hello.txt") #f))))243
(test "a trusted comment moved from another valid signature"244
;; Both signatures are genuine; splicing one's comment onto the other245
;; must fail the global signature.246
(let ((ls (lines-of "hello.txt.ED.minisig"))247
(other (lines-of "empty.txt.ED.minisig")))248
(assert-eq 'bad-trusted-comment-signature249
(minisign-failure-reason key-a250
(minisign-parse-signature251
(join-lines (with-line (with-line ls 2 (caddr other)) 3 (nth other 3))))252
(fixture "hello.txt") #f))))253
(test "tampered signature bytes"254
(let* ((ls (lines-of "hello.txt.ED.minisig"))255
(line (cadr ls)))256
(assert-eq 'bad-signature257
(minisign-failure-reason key-a258
(minisign-parse-signature259
(join-lines (with-line ls 1 (string-with-char line 50 (other-b64-char (string-ref line 50))))))260
(fixture "hello.txt") #f))))261
(test "tampered global signature bytes"262
(let* ((ls (lines-of "hello.txt.ED.minisig"))263
(line (nth ls 3)))264
(assert-eq 'bad-trusted-comment-signature265
(minisign-failure-reason key-a266
(minisign-parse-signature267
(join-lines (with-line ls 3 (string-with-char line 40 (other-b64-char (string-ref line 40))))))268
(fixture "hello.txt") #f))))269
(test "a signature by key b checked with key a"270
(assert-eq 'key-id-mismatch271
(minisign-failure-reason key-a (sig "hello.txt.keyb.minisig") (fixture "hello.txt") #f)))272
(test "key b's signature relabelled with key a's id is still refused"273
;; The key id is not authentication: with the id matching, the274
;; Ed25519 check is what rejects it.275
(let* ((ls (lines-of "hello.txt.keyb.minisig"))276
(a-id (bytevector-copy (base64-decode (cadr (lines-of "hello.txt.ED.minisig"))) 2 10))277
(relabelled (resign-line (cadr ls) 2 a-id)))278
(assert-eq 'bad-signature279
(minisign-failure-reason key-a280
(minisign-parse-signature (join-lines (with-line ls 1 relabelled)))281
(fixture "hello.txt") #f))))282
(test "legacy refused when prehashed is required"283
(assert-eq 'legacy-refused284
(minisign-failure-reason key-a (sig "hello.txt.Ed.minisig") (fixture "hello.txt") #t)))285
(test "minisign-verify-prehashed returns #f for a valid legacy signature"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 same289
;; Ed25519 signature no longer matches the file. (The algorithm bytes290
;; 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-signature294
(minisign-failure-reason key-a295
(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 -> Ed299
;; is a valid legacy signature over the 64-byte BLAKE2b digest of the300
;; file. minisign-verify-prehashed is the defence.301
(let* ((ls (lines-of "hello.txt.ED.minisig"))302
(relabelled (minisign-parse-signature303
(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"))))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 comment314
;; and 8190 for the trusted comment line. Each limit is tested at the315
;; boundary, both sides, with and without CR.316
(define (x-line prefix total)317
(string-append prefix (make-string (- total (string-length prefix)) #\x)))319
(define (sig-with-line index new)320
(join-lines (with-line (lines-of "hello.txt.ED.minisig") index new)))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-signature325
(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-signature329
(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-signature332
(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-signature335
(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-signature339
(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-signature342
(sig-with-line 2 (x-line "trusted comment: " 8190)))))343
(assert-true (raises-with? "trusted comment is too long"344
(lambda () (minisign-parse-signature345
(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-signature349
(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-key353
(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-key356
(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-key361
(string-append "\n" (fixture-text "a.pub"))))))))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)))))366
(test "minisign-failure-reason refuses an unparsed signature"367
(assert-true (raises-with? "not a parsed signature" (lambda () (minisign-failure-reason key-a (fixture-text "hello.txt.ED.minisig") "x" #f))))))