@amritk/nish-x86_64-linux 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/std/README.md CHANGED
@@ -32,19 +32,33 @@ performance gate compiles every module with no diagnostics under both
32
32
  ChaCha20-Poly1305, AES-GCM, P-256 and X.509 also run their vectors in `f64`
33
33
  (`crypto_*_f64`).
34
34
 
35
+ Every module here was security-audited (#363); the records are in
36
+ [`docs/security/`](../docs/security/README.md), and how to report a
37
+ vulnerability is in [`SECURITY.md`](../SECURITY.md). Two limits hold across the
38
+ modules. **Lengths stop at 2^31 − 1.** No array built by `push` or `new Array`,
39
+ and no array or string the runtime makes (a file read, a concatenation, a
40
+ template), can pass 2^31 − 1 elements or bytes, and the one-shot functions
41
+ below refuse a longer input as each row says. A string built by `join` still
42
+ can, and under `--number-mode i32` its `length` is then wrong, which nothing in
43
+ `std` can tell (CG-3, open): a program built in that mode must not hand these
44
+ functions anything built from such a string. **Secrets are not wiped.** Private keys,
45
+ nonces and intermediate state stay in arena memory after a call returns, until
46
+ that memory is reused, because the language has no store the optimiser is
47
+ barred from removing (ECC-2, X509-7, open).
48
+
35
49
  | Module | What it is | Reproduces |
36
50
  | --- | --- | --- |
37
- | [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK` | FIPS 180-4 §6.2 |
38
- | [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK` | FIPS 180-4 §6.4, §6.5 |
39
- | [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual` | RFC 2104, RFC 4231 §4 |
40
- | [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen. TLS 1.3's HKDF-Expand-Label is not here; it belongs with TLS | RFC 5869 §2, Appendix A |
41
- | [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public | — |
42
- | [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null` | RFC 4648 §5, §10 |
43
- | [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)` and `x25519Base(scalar)`, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy | RFC 7748 §5.2, §6.1 |
44
- | [`crypto/chacha20poly1305.ts`](./crypto/chacha20poly1305.ts) | `chacha20Poly1305Seal(key, nonce, aad, plaintext)`, which answers the ciphertext followed by the 16-byte tag, and `chacha20Poly1305Open`, which checks the whole tag before it decrypts anything and answers `null` for a message that does not authenticate. Beneath them `chacha20Block`, `chacha20`, `chacha20QuarterRound`, `poly1305` and `poly1305KeyGen`, and `chacha20HeaderMask`, QUIC's 5-byte header-protection mask. Every input of the wrong length answers `null` | RFC 8439 §2, RFC 9001 §5.4.4 |
45
- | [`crypto/aes.ts`](./crypto/aes.ts) | AES-128 and AES-256, bitsliced four blocks at a time with the Boyar–Peralta S-box circuit, so no table is read: `aesKey(key)` expands a 16- or 32-byte key into an `AesKey`, then `aesEncryptBlock`, `aesGcmSeal` / `aesGcmOpen` (tag last, checked in full before anything is decrypted; any non-empty IV) and `aesHeaderMask`. AES-192 is out of scope, and a wrong length answers `null` | FIPS 197, SP 800-38D, RFC 9001 §5.4.3, Wycheproof `aes_gcm` |
46
- | [`crypto/p256.ts`](./crypto/p256.ts) | ECDSA over P-256 with RFC 6979's deterministic nonces: `p256PublicKey(priv)` (65-byte uncompressed SEC1), `p256Sign` / `p256Verify` over a digest and `p256SignSha256` / `p256VerifySha256` over a message; a signature is `r` and `s`, 32 big-endian bytes each, not DER. A malformed key, a point off the curve or an `r` or `s` out of range answers `null` or `false`, never a panic | RFC 6979 A.2.5, Wycheproof `ecdsa_secp256r1_sha256` |
47
- | [`crypto/x509.ts`](./crypto/x509.ts) | DER, PEM and X.509 over P-256: `pemToDer` / `derToPem` (RFC 7468, padded standard base64, strict labels), `x509ParseP256PrivateKey` (SEC1 or PKCS#8), `x509ParseCertificate` / `x509ParseChain` into an `X509Certificate`, `x509VerifySignature` (ecdsa-with-SHA256 under an issuer's key), `x509MintSelfSigned`, the 1-to-14-day self-signed P-256 certificate WebTransport's `serverCertificateHashes` accepts, and `x509CertificateHash`, its SHA-256. DER is read strictly — non-minimal lengths and integers, indefinite lengths and trailing bytes answer `null` | X.690, RFC 5280, RFC 7468, RFC 5915, RFC 5208 / 5958; certificates checked against OpenSSL |
51
+ | [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK`. `sha256` panics on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.2 |
52
+ | [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK`. The one-shot `sha512` and `sha384` panic on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.4, §6.5 |
53
+ | [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual`. The one-shot functions panic on a message longer than 2^31 − 1 bytes | RFC 2104, RFC 4231 §4 |
54
+ | [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen, for an `info` longer than 2^31 − 1 bytes, and for a PRK shorter than HashLen. TLS 1.3's HKDF-Expand-Label is not here; it belongs with TLS | RFC 5869 §2, Appendix A |
55
+ | [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public, and so does an array longer than 2^31 − 1 bytes | — |
56
+ | [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null`, as does a text longer than 2^31 − 1 bytes; `base64urlEncode` panics on such an array | RFC 4648 §5, §10 |
57
+ | [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)` and `x25519Base(scalar)`, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy | RFC 7748 §5.2, §6.1, Wycheproof `x25519_test` |
58
+ | [`crypto/chacha20poly1305.ts`](./crypto/chacha20poly1305.ts) | `chacha20Poly1305Seal(key, nonce, aad, plaintext)`, which answers the ciphertext followed by the 16-byte tag, and `chacha20Poly1305Open`, which checks the whole tag before it decrypts anything and answers `null` for a message that does not authenticate. Beneath them `chacha20Block`, `chacha20`, `chacha20QuarterRound`, `poly1305` and `poly1305KeyGen`, and `chacha20HeaderMask`, QUIC's 5-byte header-protection mask. Every input of the wrong length answers `null` | RFC 8439 §2, RFC 9001 §5.4.4, Wycheproof `chacha20_poly1305` (all 325 cases) |
59
+ | [`crypto/aes.ts`](./crypto/aes.ts) | AES-128 and AES-256, bitsliced four blocks at a time with the Boyar–Peralta S-box circuit, so no table is read: `aesKey(key)` expands a 16- or 32-byte key into an `AesKey`, then `aesEncryptBlock`, `aesGcmSeal` / `aesGcmOpen` (tag last, checked in full before anything is decrypted; any non-empty IV) and `aesHeaderMask`. AES-192 is out of scope, and a wrong length answers `null`, as does an `AesKey` not made by `aesKey`; one whose H is zero opens nothing | FIPS 197, SP 800-38D, RFC 9001 §5.4.3, Wycheproof `aes_gcm` (all 316 cases; the 103 AES-192 ones check that the key is refused) |
60
+ | [`crypto/p256.ts`](./crypto/p256.ts) | ECDSA over P-256 with RFC 6979's deterministic nonces: `p256PublicKey(priv)` (65-byte uncompressed SEC1), `p256Sign` / `p256Verify` over a digest and `p256SignSha256` / `p256VerifySha256` over a message; a signature is `r` and `s`, 32 big-endian bytes each, not DER (`crypto/x509.ts`'s `x509DerSignatureRS` converts one). A malformed key, a point off the curve or an `r` or `s` out of range answers `null` or `false`, never a panic. ECDSA is malleable, and `p256Verify` accepts a high `s`: when `(r, s)` verifies, so does `(r, n − s)` | RFC 6979 A.2.5, Wycheproof `ecdsa_secp256r1_sha256`, `_sha256_p1363`, `_sha512` and `_sha512_p1363` |
61
+ | [`crypto/x509.ts`](./crypto/x509.ts) | DER, PEM and X.509 over P-256: `pemToDer` / `derToPem` (RFC 7468, padded standard base64, strict labels), `x509ParseP256PrivateKey` (SEC1 or PKCS#8), `x509ParseCertificate` / `x509ParseChain` into an `X509Certificate`, `x509VerifySignature` (ecdsa-with-SHA256 under an issuer's key), `x509DerSignatureRS` (a DER ECDSA-Sig-Value as the `r || s` `crypto/p256.ts` takes), `x509MintSelfSigned`, the 1-to-14-day self-signed P-256 certificate WebTransport's `serverCertificateHashes` accepts, and `x509CertificateHash`, its SHA-256. DER is read strictly — non-minimal lengths and integers, indefinite lengths and trailing bytes answer `null`. `x509ParseCertificate` and `x509ParseChain` parse and do not validate: there is no path validation, no extension is read or exposed, and no clock is consulted. `pemToDer` refuses a file in which any block, of any label, is malformed. `x509MintSelfSigned` refuses a common name that is not UTF-8 or holds a NUL, and its key and serial must come from `crypto.getRandomValues`, which the module cannot call itself without refusing wasm32 (X509-6) | X.690, RFC 5280, RFC 7468, RFC 5915, RFC 5208 / 5958; certificates checked against OpenSSL |
48
62
 
49
63
  Three rules hold across the modules:
50
64
 
@@ -55,8 +69,10 @@ Three rules hold across the modules:
55
69
  that is itself spent, so any `update` or `digest` on it panics. Either way,
56
70
  copy *before* `digest` when the computation has to go on.
57
71
  - **An all-zero X25519 result is returned, not refused.** It is what a
58
- low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol; TLS
59
- 1.3 (WP34 T1) makes it. A key exchange outside TLS has to make it itself.
72
+ low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol.
73
+ Nothing in this tree makes that check yet (TLS 1.3, WP34 T1, is planned to),
74
+ so every caller doing a key exchange must refuse an all-zero answer itself,
75
+ with `timingSafeEqual` against 32 zero bytes, since the answer is a secret.
60
76
  - **Constant time by construction, and verified where the check reaches.** No
61
77
  module branches on, or indexes by, a secret: comparisons OR the differences
62
78
  into one word and test it once, the ladder swaps with a mask and always runs
@@ -65,13 +81,22 @@ Three rules hold across the modules:
65
81
  rather than a table. Every branch is on a length, a loop counter or a bit
66
82
  position. WP34 N6 checks that the machine code kept that shape:
67
83
  `tests/ct-asm.js` disassembles golden fixtures on x86-64 and aarch64 and
68
- refuses a branch, a call or a secret-addressed load in them. What it holds,
69
- per module, is what that module's header says:
84
+ refuses a branch, a call, a secret-addressed load, or a divide or square
85
+ root of a secret (whose latency follows its operands) in them. It reads what
86
+ `clang -O2` emits for each target's baseline CPU, and only the functions a
87
+ fixture names: the fixtures are copies of each module's straight-line cores,
88
+ so the module functions themselves, and every loop, rest on review.
89
+ [`docs/security/ct-verification.md`](../docs/security/ct-verification.md)
90
+ lists every secret-handling function no fixture reads. What it holds, per
91
+ module, is what that module's header says:
70
92
  - `tests/cases/ct_asm_mac` pins the two loop shapes the others are built
71
- from, a full tag compare and a masked table read. `crypto/ct.ts` and
72
- `crypto/hmac.ts`'s verifiers compare in the first shape and
73
- `crypto/base64url.ts` selects by mask, but none of those K1 modules is
74
- itself in a fixture, so they remain discipline.
93
+ from, a full tag compare and a masked table read; `crypto/hmac.ts`'s
94
+ verifiers compare in the first shape.
95
+ - `tests/cases/ct_asm_k1_ct`: copies of `timingSafeEqual`'s loop at 32 and
96
+ 48 bytes and `timingSafeEqualAt`'s at a 16-byte window.
97
+ - `tests/cases/ct_asm_k1_base64url`: the two character maps verbatim, and
98
+ one encode group and one decode group. The length-driven loops, the
99
+ decoder's store guard and the encoder's string building remain discipline.
75
100
  - `tests/cases/ct_asm_x25519`: X25519's field multiply, square, add,
76
101
  subtract and multiply by a24, its conditional swap, and one step of the
77
102
  Montgomery ladder, the check following each call into the field functions.
package/std/crypto/aes.ts CHANGED
@@ -59,10 +59,11 @@
59
59
  *
60
60
  * **What is refused.** A key of any length but 16 or 32 bytes (AES-192 is out
61
61
  * of scope), a block or a sample that is not 16 bytes, an empty IV (SP 800-38D
62
- * §5.2.1.1 asks for at least one bit), a sealed input shorter than its tag and
63
- * a plaintext too long for its sealed form to be an array answer `null`; so
64
- * does a tag that does not verify. An IV of any other non-zero
65
- * length is hashed into the first counter block as §7.1 says.
62
+ * §5.2.1.1 asks for at least one bit), a sealed input shorter than its tag, a
63
+ * plaintext too long for its sealed form to be an array, and an `AesKey`
64
+ * `aesKey` did not make answer `null`; so does a tag that does not verify. An
65
+ * IV of any other non-zero length is hashed into the first counter block as
66
+ * §7.1 says.
66
67
  *
67
68
  * Private names carry the `aes` / `ghash` prefix because a `std/` module's
68
69
  * private functions share the importing program's flat symbol namespace
@@ -80,6 +81,12 @@ const AES_BATCH: i32 = 64
80
81
  /**
81
82
  * An AES key, expanded: the round keys already bitsliced, and GCM's hash key
82
83
  * H = E(K, 0^128), so a key used for many messages is expanded once.
84
+ *
85
+ * Make one with `aesKey`. The constructor and the fields are public only
86
+ * because the language has no private ones, and a key built or edited by hand
87
+ * is not trusted: one whose shape is not what `aesKey` makes answers `null`
88
+ * everywhere (`aesKeyUsable`), and one whose H is zero opens nothing
89
+ * (`aesGcmOpen`).
83
90
  */
84
91
  export class AesKey {
85
92
  /** Nr: 10 for AES-128, 14 for AES-256. */
@@ -574,6 +581,19 @@ const aesCopyPrefix = (dst: u8[], src: u8[], count: i32): void => {
574
581
  }
575
582
  }
576
583
 
584
+ /**
585
+ * Whether `key` has the shape `aesKey` makes: 10 or 14 rounds and the
586
+ * `(rounds + 1) * 8` round-key words the cipher reads. A key of another shape
587
+ * could only have been built or edited by hand, and would index past its round
588
+ * keys and panic, so every entry point answers `null` for it instead. The
589
+ * branch is on the shape, which is not secret.
590
+ */
591
+ const aesKeyUsable = (key: AesKey): boolean => {
592
+ const rounds: i32 = key.rounds
593
+ const words: i32 = (rounds + 1) * 8
594
+ return (rounds === 10 || rounds === 14) && toI32(key.roundKeys.length) === words
595
+ }
596
+
577
597
  /**
578
598
  * The first 16 bytes of `block` encrypted, in the first 16 bytes of a fresh
579
599
  * batch; the rest of the batch is the cipher of zeros and is not read.
@@ -605,7 +625,7 @@ export const aesKey = (key: u8[]): AesKey | null => {
605
625
 
606
626
  /** One block encrypted (FIPS 197 §5.1, ECB): 16 bytes in, 16 fresh bytes out, or `null` for another length. */
607
627
  export const aesEncryptBlock = (key: AesKey, block: u8[]): u8[] | null => {
608
- if (toI32(block.length) !== AES_BLOCK) {
628
+ if (!aesKeyUsable(key) || toI32(block.length) !== AES_BLOCK) {
609
629
  return null
610
630
  }
611
631
  const out: u8[] = new Array<u8>(AES_BLOCK)
@@ -619,7 +639,7 @@ export const aesEncryptBlock = (key: AesKey, block: u8[]): u8[] | null => {
619
639
  * a sample of another length.
620
640
  */
621
641
  export const aesHeaderMask = (key: AesKey, sample: u8[]): u8[] | null => {
622
- if (toI32(sample.length) !== AES_BLOCK) {
642
+ if (!aesKeyUsable(key) || toI32(sample.length) !== AES_BLOCK) {
623
643
  return null
624
644
  }
625
645
  const mask: u8[] = new Array<u8>(5)
@@ -781,6 +801,17 @@ const aesGcmJ0 = (key: AesKey, iv: u8[]): u8[] => {
781
801
  return j0
782
802
  }
783
803
 
804
+ /**
805
+ * Whether GCTR can encrypt `length` bytes from inc32(J0) without its 32-bit
806
+ * counter coming back round to J0's, whose block masks the tag: SP 800-38D
807
+ * §5.2.1.1 caps a plaintext at 2^32 - 2 blocks (2^39 - 256 bits). An array
808
+ * length is below 2^31, so this never refuses today; it is here so that the
809
+ * bound is stated where the counter is, rather than left to the width of an
810
+ * array length.
811
+ */
812
+ const aesGcmLengthFits = (length: i32): boolean =>
813
+ (toI64(length) + toI64(15)) >> toI64(4) <= (toI64(1) << toI64(32)) - toI64(2)
814
+
784
815
  /**
785
816
  * GCTR (SP 800-38D §6.5) from inc32(J0): `dst[i] = src[i] ⊕ keystream[i]` for
786
817
  * `i` below `length`, four blocks per pass of the cipher. The counter is the
@@ -844,16 +875,16 @@ export const aesGcmTagMask = (tagHi: u64, tagLo: u64, gotHi: u64, gotLo: u64): u
844
875
  /**
845
876
  * GCM authenticated encryption (SP 800-38D §7.1): the ciphertext followed by
846
877
  * the 16-byte tag, in one fresh array. The IV may be any non-zero length; 12
847
- * bytes is the fast and usual one. `null` for an empty IV, and for a
848
- * plaintext longer than 2^31 - 17 bytes, whose sealed form would not fit in an
849
- * array.
878
+ * bytes is the fast and usual one. `null` for an empty IV, for a key
879
+ * `aesKey` did not make, and for a plaintext longer than 2^31 - 17 bytes,
880
+ * whose sealed form would not fit in an array.
850
881
  */
851
882
  export const aesGcmSeal = (key: AesKey, iv: u8[], aad: u8[], plaintext: u8[]): u8[] | null => {
852
883
  const length: i32 = toI32(plaintext.length)
853
884
  // 2^31 - 17: the longest plaintext whose sealed form, sixteen bytes longer,
854
885
  // is still an array length.
855
886
  const longest: i32 = 0x7fffffef
856
- if (toI32(iv.length) === 0 || length > longest) {
887
+ if (!aesKeyUsable(key) || toI32(iv.length) === 0 || length > longest || !aesGcmLengthFits(length)) {
857
888
  return null
858
889
  }
859
890
  const j0: u8[] = aesGcmJ0(key, iv)
@@ -869,18 +900,22 @@ export const aesGcmSeal = (key: AesKey, iv: u8[], aad: u8[], plaintext: u8[]): u
869
900
  /**
870
901
  * GCM authenticated decryption (SP 800-38D §7.2): `sealed` is the ciphertext
871
902
  * followed by its 16-byte tag, and the answer is the plaintext, or `null` when
872
- * the tag does not verify, the IV is empty or `sealed` is shorter than a tag.
903
+ * the tag does not verify, the IV is empty, `sealed` is shorter than a tag or
904
+ * the key is not one `aesKey` made.
873
905
  * The tag is computed over the received ciphertext and compared in constant
874
906
  * time before anything is decrypted, so a forgery yields no plaintext at all.
875
907
  */
876
908
  export const aesGcmOpen = (key: AesKey, iv: u8[], aad: u8[], sealed: u8[]): u8[] | null => {
877
909
  const total: i32 = toI32(sealed.length)
878
- if (toI32(iv.length) === 0 || total < AES_GCM_TAG_SIZE) {
910
+ if (!aesKeyUsable(key) || toI32(iv.length) === 0 || total < AES_GCM_TAG_SIZE) {
879
911
  return null
880
912
  }
881
913
  // `total` is an array length and at least a tag, so neither this nor the
882
914
  // tag's second word at `length + 8` can leave the range of an `i32`.
883
915
  const length: i32 = total - AES_GCM_TAG_SIZE
916
+ if (!aesGcmLengthFits(length)) {
917
+ return null
918
+ }
884
919
  const j0: u8[] = aesGcmJ0(key, iv)
885
920
  const tag: u64[] = new Array<u64>(2)
886
921
  aesGcmTag(tag, key, j0, aad, sealed, length)
@@ -890,7 +925,12 @@ export const aesGcmOpen = (key: AesKey, iv: u8[], aad: u8[], sealed: u8[]): u8[]
890
925
  aesLoad64(sealed, length, total),
891
926
  aesLoad64(sealed, length + 8, total)
892
927
  )
893
- if (same === toU64(0)) {
928
+ // Under H = 0 GHASH is zero for every input, so the tag E(K, J0) would
929
+ // authenticate any ciphertext and AAD. Only a key built or edited by hand
930
+ // has it (for one from `aesKey` the chance is 2^-128), and such a key opens
931
+ // nothing. H is secret, so this is folded into the mask, not branched on.
932
+ const hashKeySet: u64 = ~ctEq(key.hHi | key.hLo, toU64(0))
933
+ if ((same & hashKeySet) === toU64(0)) {
894
934
  return null
895
935
  }
896
936
  const out: u8[] = new Array<u8>(length)
@@ -9,9 +9,10 @@
9
9
  * **Decoding is strict, so that each byte string has exactly one spelling.**
10
10
  * `base64urlDecode` answers `null` for a `=` anywhere, for any character outside
11
11
  * the alphabet (whitespace included), for a length of 1 mod 4 — six bits, not a
12
- * byte — and for a final character whose unused low bits are not zero (RFC 4648
13
- * §3.5). A lenient decoder would let `AB` and `AA` both mean `00`, and a token
14
- * compared or cached by its text would then have two identities.
12
+ * byte — for a final character whose unused low bits are not zero (RFC 4648
13
+ * §3.5), and for a text longer than 2^31 - 1 bytes. A lenient decoder would let
14
+ * `AB` and `AA` both mean `00`, and a token compared or cached by its text
15
+ * would then have two identities.
15
16
  *
16
17
  * **Neither direction indexes or branches on the data.** What is encoded here
17
18
  * is usually a key, a nonce or a MAC, and a lookup table indexed by a secret
@@ -69,9 +70,15 @@ const base64urlSextetOf = (c: i32): i32 => {
69
70
  *
70
71
  * Bytes go into a bit accumulator eight at a time and come out six at a time,
71
72
  * so one index walks the input and the only branches are on how many bits are
72
- * waiting — which depends on the position, never on the bytes.
73
+ * waiting — which depends on the position, never on the bytes. An array longer
74
+ * than 2^31 - 1 bytes panics.
73
75
  */
74
76
  export const base64urlEncode = (data: u8[]): string => {
77
+ // Refused before `toI32`, which saturates a longer length under
78
+ // `--number-mode f64` and would end the loop at a prefix.
79
+ if (data.length > 2147483647) {
80
+ panic("base64urlEncode: more than 2^31 - 1 bytes")
81
+ }
75
82
  const parts: string[] = []
76
83
  let acc: i32 = 0
77
84
  let bits: i32 = 0
@@ -96,12 +103,18 @@ export const base64urlEncode = (data: u8[]): string => {
96
103
  /**
97
104
  * The bytes `text` spells in unpadded base64url, or `null` when it is not the
98
105
  * one canonical spelling of any byte string — see the module comment for the
99
- * four refusals.
106
+ * five refusals.
100
107
  *
101
108
  * The same accumulator as `base64urlEncode`, run the other way: six bits in per
102
109
  * character, eight out per byte.
103
110
  */
104
111
  export const base64urlDecode = (text: string): u8[] | null => {
112
+ // A text past 2^31 - 1 bytes is refused before `toI32`, which would
113
+ // saturate it under `--number-mode f64` and decode only a prefix: a second
114
+ // spelling of that prefix's bytes, which strictness exists to refuse.
115
+ if (text.length > 2147483647) {
116
+ return null
117
+ }
105
118
  const n: i32 = toI32(text.length)
106
119
  const tail: i32 = n & 3
107
120
  // Six bits cannot finish a byte, so a single character after the last full
@@ -274,7 +274,10 @@ const chacha20Poly1305Setup = (state: u32[]): u8[] => {
274
274
  * Whether the blocks `len` bytes need, from `counter` on, fit before the
275
275
  * 32-bit counter wraps.
276
276
  * RFC 8439 §2.4 leaves a wrap undefined, and a wrapped counter repeats the
277
- * keystream, so a request that would need one answers `null` instead.
277
+ * keystream, so a request that would need one answers `null` instead. The
278
+ * AEAD asks it too, from counter 1: an array length is below 2^31, so that
279
+ * never refuses today, but the bound is then stated where the counter is
280
+ * rather than left to the width of an array length.
278
281
  */
279
282
  const chacha20CounterFits = (counter: u32, len: i32): boolean => {
280
283
  const blocks: i64 = (toI64(len) + toI64(63)) >> toI64(6)
@@ -634,7 +637,8 @@ export const chacha20Poly1305Seal = (key: u8[], nonce: u8[], aad: u8[], plaintex
634
637
  if (
635
638
  toI32(key.length) !== CHACHA20_KEY_SIZE ||
636
639
  toI32(nonce.length) !== CHACHA20POLY1305_NONCE_SIZE ||
637
- len > longest
640
+ len > longest ||
641
+ !chacha20CounterFits(1, len)
638
642
  ) {
639
643
  return null
640
644
  }
@@ -668,6 +672,9 @@ export const chacha20Poly1305Open = (key: u8[], nonce: u8[], aad: u8[], sealed:
668
672
  return null
669
673
  }
670
674
  const len: i32 = total - POLY1305_TAG_SIZE
675
+ if (!chacha20CounterFits(1, len)) {
676
+ return null
677
+ }
671
678
  const state: u32[] = chacha20State(key, 0, nonce, 0)
672
679
  const polyKey: u8[] = chacha20Poly1305Setup(state)
673
680
  const tag: u8[] = chacha20Poly1305Tag(polyKey, aad, sealed, 0, len)
package/std/crypto/ct.ts CHANGED
@@ -23,12 +23,18 @@
23
23
  *
24
24
  * Arrays of different lengths answer `false` at once, because a length is
25
25
  * public; arrays of one length are compared in full, with no early exit.
26
+ *
27
+ * The lengths are compared as the `number`s they are, before any `toI32`:
28
+ * under `--number-mode f64` `toI32` saturates at 2^31 - 1, so two arrays past
29
+ * it would have looked the same length and been compared only that far. An
30
+ * array longer than that answers `false` too — no `i32` index reaches its end,
31
+ * and a compare that cannot read every byte must not say "equal".
26
32
  */
27
33
  export const timingSafeEqual = (a: u8[], b: u8[]): boolean => {
28
- const n: i32 = toI32(a.length)
29
- if (n !== toI32(b.length)) {
34
+ if (a.length !== b.length || a.length > 2147483647) {
30
35
  return false
31
36
  }
37
+ const n: i32 = toI32(a.length)
32
38
  let diff: i32 = 0
33
39
  // Bounded by both lengths, which are equal here, so the prover drops both
34
40
  // bounds checks rather than trusting the comparison above.
@@ -16,7 +16,11 @@
16
16
  * The counter `i` is one byte, so `L` is at most 255 × HashLen (§2.3): 8160
17
17
  * bytes for SHA-256 and 12240 for SHA-384. A longer `L`, or a negative one,
18
18
  * answers `null` rather than panicking, because `L` usually comes from a
19
- * protocol field; `L = 0` answers an empty array.
19
+ * protocol field; `L = 0` answers an empty array. So does an `info` longer
20
+ * than 2^31 - 1 bytes, which `toI32` would cut to a prefix under
21
+ * `--number-mode f64`, so that two `info`s sharing it gave one key, and a PRK
22
+ * shorter than HashLen, which §2.3 does not allow: an empty one is a key
23
+ * anybody can compute with.
20
24
  *
21
25
  * HKDF-Expand-Label, TLS 1.3's wrapper around `expand`, belongs with TLS and is
22
26
  * not here. Written from RFC 5869, not ported from another implementation.
@@ -64,10 +68,16 @@ export const hkdfExtractSha384 = (salt: u8[], ikm: u8[]): u8[] => hmacSha384(hkd
64
68
  /**
65
69
  * HKDF-Expand with HMAC-SHA-256 (RFC 5869 §2.3): `length` bytes of output
66
70
  * keying material from `prk` and `info`, or `null` when `length` is negative
67
- * or past 255 × 32.
71
+ * or past 255 × 32, when `info` is longer than 2^31 - 1 bytes, or when `prk`
72
+ * is shorter than 32 bytes.
68
73
  */
69
74
  export const hkdfExpandSha256 = (prk: u8[], info: u8[], length: i32): u8[] | null => {
70
- if (length < 0 || length > HKDF_MAX_BLOCKS * SHA256_SIZE) {
75
+ if (
76
+ length < 0 ||
77
+ length > HKDF_MAX_BLOCKS * SHA256_SIZE ||
78
+ info.length > 2147483647 ||
79
+ toI32(prk.length) < SHA256_SIZE
80
+ ) {
71
81
  return null
72
82
  }
73
83
  const out: u8[] = new Array<u8>(length)
@@ -92,10 +102,16 @@ export const hkdfExpandSha256 = (prk: u8[], info: u8[], length: i32): u8[] | nul
92
102
  /**
93
103
  * HKDF-Expand with HMAC-SHA-384 (RFC 5869 §2.3): `length` bytes of output
94
104
  * keying material from `prk` and `info`, or `null` when `length` is negative
95
- * or past 255 × 48.
105
+ * or past 255 × 48, when `info` is longer than 2^31 - 1 bytes, or when `prk`
106
+ * is shorter than 48 bytes.
96
107
  */
97
108
  export const hkdfExpandSha384 = (prk: u8[], info: u8[], length: i32): u8[] | null => {
98
- if (length < 0 || length > HKDF_MAX_BLOCKS * SHA384_SIZE) {
109
+ if (
110
+ length < 0 ||
111
+ length > HKDF_MAX_BLOCKS * SHA384_SIZE ||
112
+ info.length > 2147483647 ||
113
+ toI32(prk.length) < SHA384_SIZE
114
+ ) {
99
115
  return null
100
116
  }
101
117
  const out: u8[] = new Array<u8>(length)
@@ -127,15 +127,31 @@ export class HmacSha384 {
127
127
  }
128
128
  }
129
129
 
130
- /** The HMAC-SHA-256 of all of `data` under `key`, as a fresh 32-byte array. */
130
+ /**
131
+ * The HMAC-SHA-256 of all of `data` under `key`, as a fresh 32-byte array.
132
+ *
133
+ * `data` longer than 2^31 - 1 bytes panics, as `sha256` does and for its
134
+ * reason: under `--number-mode f64` the tag would be of a prefix, and a tag
135
+ * that verifies every message sharing that prefix is a forgery. A key that
136
+ * long reaches `sha256` in the constructor and panics there.
137
+ */
131
138
  export const hmacSha256 = (key: u8[], data: u8[]): u8[] => {
139
+ if (data.length > 2147483647) {
140
+ panic("hmacSha256: a message longer than 2^31 - 1 bytes")
141
+ }
132
142
  const mac = new HmacSha256(key)
133
143
  mac.update(data, HMAC_FROM, toI32(data.length))
134
144
  return mac.digest()
135
145
  }
136
146
 
137
- /** The HMAC-SHA-384 of all of `data` under `key`, as a fresh 48-byte array. */
147
+ /**
148
+ * The HMAC-SHA-384 of all of `data` under `key`, as a fresh 48-byte array.
149
+ * `data` longer than 2^31 - 1 bytes panics, as in `hmacSha256`.
150
+ */
138
151
  export const hmacSha384 = (key: u8[], data: u8[]): u8[] => {
152
+ if (data.length > 2147483647) {
153
+ panic("hmacSha384: a message longer than 2^31 - 1 bytes")
154
+ }
139
155
  const mac = new HmacSha384(key)
140
156
  mac.update(data, HMAC_FROM, toI32(data.length))
141
157
  return mac.digest()
@@ -4273,6 +4273,11 @@ export const p256Sign = (priv: u8[], digest: u8[]): u8[] | null => {
4273
4273
  * that is not a point on the curve, and a sum that is the identity all answer
4274
4274
  * `false`. Every value here is public, so the answer may be reached by
4275
4275
  * branches; the multiplications are the constant-time ones all the same.
4276
+ *
4277
+ * ECDSA is malleable, and so is this: when `(r, s)` verifies, `(r, n - s)`
4278
+ * verifies too, as FIPS 186-4 and SEC 1 have it. Nothing here asks for the
4279
+ * lower `s`, so a protocol that needs one signature per message and key (a
4280
+ * signature used as an identifier, say) has to refuse the upper half itself.
4276
4281
  */
4277
4282
  export const p256Verify = (pub: u8[], digest: u8[], sig: u8[]): boolean => {
4278
4283
  if (toI32(sig.length) !== 64) {
@@ -435,8 +435,19 @@ export class Sha256 {
435
435
  }
436
436
  }
437
437
 
438
- /** The SHA-256 digest of all of `data`, as a fresh 32-byte array. */
438
+ /**
439
+ * The SHA-256 digest of all of `data`, as a fresh 32-byte array.
440
+ *
441
+ * An array longer than 2^31 - 1 bytes panics. A window's length is an `i32`,
442
+ * and under `--number-mode f64` `toI32` of a longer length saturates, so
443
+ * hashing `toI32(data.length)` bytes would answer the digest of a prefix: one
444
+ * digest for every message that shares its first 2^31 - 1 bytes. The length
445
+ * is compared as the `number` it is, before any `toI32`.
446
+ */
439
447
  export const sha256 = (data: u8[]): u8[] => {
448
+ if (data.length > 2147483647) {
449
+ panic("sha256: a message longer than 2^31 - 1 bytes")
450
+ }
440
451
  const hasher: Sha256 = new Sha256()
441
452
  const from: i32 = 0
442
453
  hasher.update(data, from, toI32(data.length))
@@ -495,15 +495,29 @@ export class Sha384 {
495
495
  }
496
496
  }
497
497
 
498
- /** The SHA-512 digest of all of `data`, as a fresh 64-byte array. */
498
+ /**
499
+ * The SHA-512 digest of all of `data`, as a fresh 64-byte array. An array
500
+ * longer than 2^31 - 1 bytes panics, for the reason `sha256` gives: a longer
501
+ * length does not survive `toI32` under `--number-mode f64`, and the digest
502
+ * would be of a prefix.
503
+ */
499
504
  export const sha512 = (data: u8[]): u8[] => {
505
+ if (data.length > 2147483647) {
506
+ panic("sha512: a message longer than 2^31 - 1 bytes")
507
+ }
500
508
  const hasher = new Sha512()
501
509
  hasher.update(data, OFFSET_ZERO, toI32(data.length))
502
510
  return hasher.digest()
503
511
  }
504
512
 
505
- /** The SHA-384 digest of all of `data`, as a fresh 48-byte array. */
513
+ /**
514
+ * The SHA-384 digest of all of `data`, as a fresh 48-byte array. An array
515
+ * longer than 2^31 - 1 bytes panics, as in `sha512`.
516
+ */
506
517
  export const sha384 = (data: u8[]): u8[] => {
518
+ if (data.length > 2147483647) {
519
+ panic("sha384: a message longer than 2^31 - 1 bytes")
520
+ }
507
521
  const hasher = new Sha384()
508
522
  hasher.update(data, OFFSET_ZERO, toI32(data.length))
509
523
  return hasher.digest()
@@ -480,9 +480,10 @@ const x25519Ladder = (k: u8[], u: i64[]): u8[] => {
480
480
  *
481
481
  * Answers `null` unless both arguments are exactly `X25519_SIZE` bytes. An
482
482
  * all-zero answer is **returned, not refused**: it is what a low-order `u`
483
- * gives, and RFC 7748 §6.1 leaves checking for it to the protocol, which is
484
- * where TLS 1.3 (WP34 T1) makes that check. A caller doing a key exchange
485
- * outside TLS should make it too.
483
+ * gives, and RFC 7748 §6.1 leaves checking for it to the protocol. Nothing in
484
+ * this tree makes that check for you yet (TLS 1.3, WP34 T1, is planned to), so
485
+ * every caller doing a key exchange must refuse an all-zero answer itself —
486
+ * with `timingSafeEqual` against 32 zero bytes, since the answer is a secret.
486
487
  */
487
488
  export const x25519 = (scalar: u8[], u: u8[]): u8[] | null => {
488
489
  if (toI32(scalar.length) !== 32 || toI32(u.length) !== 32) {