kxco-post-quantum 1.2.1 → 1.3.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/CHANGELOG.md +36 -0
- package/README.md +39 -1
- package/SECURITY.md +16 -3
- package/package.json +6 -7
- package/src/_context.js +69 -0
- package/src/ml-dsa.d.ts +36 -3
- package/src/ml-dsa.js +28 -4
- package/src/slh-dsa.d.ts +29 -3
- package/src/slh-dsa.js +22 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 - unreleased
|
|
4
|
+
|
|
5
|
+
Adds FIPS 204 / FIPS 205 context string support. Purely additive: the
|
|
6
|
+
cryptographic surface for every existing call site is byte-for-byte unchanged,
|
|
7
|
+
and all 39 pinned vectors still match.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Optional `context` on `mlDsa.sign` / `mlDsa.verify`** via a trailing options
|
|
11
|
+
object, `{ context }`. At most 255 bytes per FIPS 204 section 5.2; strings are
|
|
12
|
+
encoded as UTF-8. A signature made under a context does not verify without it
|
|
13
|
+
or under a different one. Closes a real incompleteness relative to FIPS 204:
|
|
14
|
+
the library previously could not verify a counterparty's signature that used a
|
|
15
|
+
context.
|
|
16
|
+
- **The same option on `slhDsa.sign` / `slhDsa.verify`**, on the same terms.
|
|
17
|
+
Added alongside ML-DSA rather than after it, because one signature module
|
|
18
|
+
accepting an options object while its sibling silently ignored one would be a
|
|
19
|
+
footgun.
|
|
20
|
+
- `MAX_CONTEXT_BYTES` (255) exported from both modules.
|
|
21
|
+
- `test/context.test.js`, 26 tests, including cross-verification against
|
|
22
|
+
third-party ML-DSA-65 signatures produced by OpenSSL through Python
|
|
23
|
+
`cryptography`, covering five context shapes: short, single-byte, the 255-byte
|
|
24
|
+
maximum, binary, and multi-byte UTF-8.
|
|
25
|
+
|
|
26
|
+
### Notes
|
|
27
|
+
- **No behaviour change without the new argument.** Omitting `opts` takes the
|
|
28
|
+
identical code path as before. An empty context is collapsed to no context,
|
|
29
|
+
which is what FIPS 204 specifies and what was verified empirically.
|
|
30
|
+
- **Misuse throws rather than returning `false`.** A context over 255 bytes, a
|
|
31
|
+
wrongly typed context, or a bare value passed where an options object belongs
|
|
32
|
+
raises `RangeError` / `TypeError`. These are caller bugs, not failed
|
|
33
|
+
verifications, and swallowing them would hide a signature that silently
|
|
34
|
+
carried no domain separation. No existing call site passes the argument, so
|
|
35
|
+
nothing can regress.
|
|
36
|
+
- Works identically on `@noble/post-quantum` 0.6.1 and 0.7.0, verified on both,
|
|
37
|
+
so this release is independent of the 0.7.0 upgrade.
|
|
38
|
+
|
|
3
39
|
## 1.2.1 — 2026-07-22
|
|
4
40
|
|
|
5
41
|
Metadata alignment. No code changes; cryptographic surface is byte-for-byte
|
package/README.md
CHANGED
|
@@ -48,6 +48,44 @@ const recovered = mlKem.decapsulate(ciphertext, kemKeys.secretKey)
|
|
|
48
48
|
|
|
49
49
|
`masterSecret` is a `Buffer` or `Uint8Array` with at least 16 bytes of entropy (typically 32–64 bytes from an env var or KMS).
|
|
50
50
|
|
|
51
|
+
### Context strings (FIPS 204 / FIPS 205)
|
|
52
|
+
|
|
53
|
+
`sign` and `verify` take an optional context string, at most 255 bytes. A
|
|
54
|
+
signature made under a context does not verify without it, or under a different
|
|
55
|
+
one.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const sig = mlDsa.sign(secretKey, 'hello', { context: 'kxco-nexus-v1' })
|
|
59
|
+
|
|
60
|
+
mlDsa.verify(publicKey, 'hello', sig, { context: 'kxco-nexus-v1' }) // true
|
|
61
|
+
mlDsa.verify(publicKey, 'hello', sig) // false
|
|
62
|
+
mlDsa.verify(publicKey, 'hello', sig, { context: 'other-v1' }) // false
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The parameter is optional and defaults to no context, so every existing call
|
|
66
|
+
site is unaffected. An empty context is identical to omitting it. `slhDsa` takes
|
|
67
|
+
the same option.
|
|
68
|
+
|
|
69
|
+
**Context separates at the signature level; `keypairFromMaster(master, info)`
|
|
70
|
+
separates at the key level.** They are complementary. Use a context when one key
|
|
71
|
+
legitimately signs for several purposes and you need a signature from one
|
|
72
|
+
purpose to be unusable in another. Use a distinct derived key when the purposes
|
|
73
|
+
should not share a key at all.
|
|
74
|
+
|
|
75
|
+
Strings are encoded as UTF-8, so the 255-byte limit is bytes and not
|
|
76
|
+
characters. Over-length or wrongly typed input throws (`RangeError` /
|
|
77
|
+
`TypeError`) rather than returning `false`, because that is a caller bug and not
|
|
78
|
+
a failed verification:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
mlDsa.sign(secretKey, 'hello', 'kxco-nexus-v1') // throws TypeError
|
|
82
|
+
// (needs { context: ... })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That last case is worth guarding: without the throw it would silently sign with
|
|
86
|
+
*no* context and produce a valid-looking signature carrying none of the intended
|
|
87
|
+
separation.
|
|
88
|
+
|
|
51
89
|
---
|
|
52
90
|
|
|
53
91
|
## API
|
|
@@ -129,7 +167,7 @@ Install this package directly when you need ML-DSA or ML-KEM without the rest of
|
|
|
129
167
|
|
|
130
168
|
Cryptographic operations delegate entirely to [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) and [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) — this package does not reimplement any NIST primitive. `@noble/hashes` falls under Cure53's 2023 audit of the `@noble` ecosystem (`ciphers`, `curves`, `hashes`); `@noble/post-quantum` was **not** in that audit's scope and has been self-audited by its maintainer. See [AUDIT.md](./AUDIT.md) for the full posture.
|
|
131
169
|
|
|
132
|
-
To report a vulnerability: [open a private security advisory](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/security/advisories/new) or email **
|
|
170
|
+
To report a vulnerability: [open a private security advisory](https://github.com/KnightsbridgeAIQ/kxco-post-quantum/security/advisories/new) or email **john@knightsbridgelaw.com**. Acknowledgement within 2 business days, triage decision within 5. Full policy, including safe harbour for good-faith research: <https://kxco.ai/security>.
|
|
133
171
|
|
|
134
172
|
## License
|
|
135
173
|
|
package/SECURITY.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# Security Policy
|
|
2
2
|
|
|
3
3
|
## Reporting a vulnerability
|
|
4
|
-
Email **
|
|
5
|
-
PGP key available on request. We
|
|
6
|
-
|
|
4
|
+
Email **john@knightsbridgelaw.com**. Do not open public issues for security reports.
|
|
5
|
+
PGP key available on request. We credit reporters in `CHANGELOG.md` unless they
|
|
6
|
+
request otherwise.
|
|
7
|
+
|
|
8
|
+
Acknowledgement within **2 business days**. Triage decision within **5 business days**.
|
|
9
|
+
|
|
10
|
+
Full policy: <https://kxco.ai/security>
|
|
11
|
+
|
|
12
|
+
## Safe harbour
|
|
13
|
+
If you make a good-faith effort to comply with this policy, we will treat your
|
|
14
|
+
research as authorised, and we will not pursue or support legal action against
|
|
15
|
+
you. Good faith means: do not access, modify, exfiltrate, or destroy data that
|
|
16
|
+
is not yours; use test accounts where possible; do not degrade service for
|
|
17
|
+
others; and stop and report as soon as you have established that a
|
|
18
|
+
vulnerability exists. This cannot bind third parties, and it does not cover
|
|
19
|
+
extortion, data sale, or public disclosure ahead of the window below.
|
|
7
20
|
|
|
8
21
|
## Scope
|
|
9
22
|
In scope:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kxco-post-quantum",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "ML-DSA-65, ML-KEM-768 and SLH-DSA-SHA2-192s primitives with key fingerprinting. The base layer for all kxco-pq-* packages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"deterministic-keys",
|
|
24
24
|
"fingerprint",
|
|
25
25
|
"replay-protection",
|
|
26
|
-
"constant-time",
|
|
27
26
|
"hybrid-signing",
|
|
28
27
|
"non-repudiation"
|
|
29
28
|
],
|
|
@@ -41,10 +40,10 @@
|
|
|
41
40
|
"funding": "https://kxco.ai",
|
|
42
41
|
"repository": {
|
|
43
42
|
"type": "git",
|
|
44
|
-
"url": "https://github.com/
|
|
43
|
+
"url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum.git"
|
|
45
44
|
},
|
|
46
45
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/
|
|
46
|
+
"url": "https://github.com/KnightsbridgeAIQ/kxco-post-quantum/issues"
|
|
48
47
|
},
|
|
49
48
|
"type": "module",
|
|
50
49
|
"sideEffects": false,
|
|
@@ -91,11 +90,11 @@
|
|
|
91
90
|
"node": ">=20.19"
|
|
92
91
|
},
|
|
93
92
|
"dependencies": {
|
|
94
|
-
"@noble/hashes": "
|
|
95
|
-
"@noble/post-quantum": "
|
|
93
|
+
"@noble/hashes": "2.3.0",
|
|
94
|
+
"@noble/post-quantum": "0.7.0"
|
|
96
95
|
},
|
|
97
96
|
"scripts": {
|
|
98
|
-
"test": "node --test test/basic.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
|
|
97
|
+
"test": "node --test test/basic.test.js && node --test test/context.test.js && node --test test/browser-smoke.test.js && node test/run-vectors.js",
|
|
99
98
|
"test:vectors": "node test/run-vectors.js",
|
|
100
99
|
"generate:vectors": "node test/generate-vectors.js > test/vectors.json",
|
|
101
100
|
"bench": "node bench/bench.js"
|
package/src/_context.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Internal: FIPS 204 / FIPS 205 signature context strings.
|
|
2
|
+
//
|
|
3
|
+
// Not part of the public exports map. Shared by ml-dsa.js and slh-dsa.js
|
|
4
|
+
// because a validator for security-relevant input should exist once, even
|
|
5
|
+
// though the small byte/hex helpers in those modules are duplicated.
|
|
6
|
+
//
|
|
7
|
+
// FIPS 204 section 5.2 (and FIPS 205 equivalently) allow an optional context
|
|
8
|
+
// string of at most 255 bytes, mixed into the message representative. It gives
|
|
9
|
+
// domain separation: a signature made under one context does not verify under
|
|
10
|
+
// another, or under no context at all.
|
|
11
|
+
//
|
|
12
|
+
// KXCO derives keys per domain via deriveSeed(master, info), which separates at
|
|
13
|
+
// the KEY level. Context separates at the SIGNATURE level, so one key can sign
|
|
14
|
+
// for several domains without a signature being replayable across them. The two
|
|
15
|
+
// are complementary, not alternatives.
|
|
16
|
+
|
|
17
|
+
const enc = new TextEncoder()
|
|
18
|
+
|
|
19
|
+
/** FIPS 204 section 5.2: |ctx| <= 255. */
|
|
20
|
+
export const MAX_CONTEXT_BYTES = 255
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalise the options bag into a context byte string, or undefined.
|
|
24
|
+
*
|
|
25
|
+
* Returns undefined for "no context", which makes the caller take the exact
|
|
26
|
+
* code path it took before this parameter existed. An empty context is
|
|
27
|
+
* cryptographically identical to no context, so it also returns undefined
|
|
28
|
+
* rather than passing a zero-length array down.
|
|
29
|
+
*
|
|
30
|
+
* THROWS on caller misuse (wrong type, over-length). This is deliberate and it
|
|
31
|
+
* differs from verify()'s usual fail-closed behaviour: a malformed context is a
|
|
32
|
+
* programming error, not a bad signature, and silently returning false would
|
|
33
|
+
* hide the bug behind an outcome that looks like a normal verification failure.
|
|
34
|
+
* No existing call site passes this argument, so nothing can regress.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts]
|
|
37
|
+
* @returns {Uint8Array|undefined}
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeContext(opts) {
|
|
40
|
+
if (opts === undefined || opts === null) return undefined
|
|
41
|
+
|
|
42
|
+
if (typeof opts !== 'object' || Array.isArray(opts) || opts instanceof Uint8Array) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
'expected an options object such as { context }, not a bare value',
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { context } = opts
|
|
49
|
+
if (context === undefined || context === null) return undefined
|
|
50
|
+
|
|
51
|
+
let bytes
|
|
52
|
+
if (context instanceof Uint8Array) {
|
|
53
|
+
bytes = context
|
|
54
|
+
} else if (typeof context === 'string') {
|
|
55
|
+
bytes = enc.encode(context)
|
|
56
|
+
} else {
|
|
57
|
+
throw new TypeError('context must be a Uint8Array or a string')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (bytes.length > MAX_CONTEXT_BYTES) {
|
|
61
|
+
throw new RangeError(
|
|
62
|
+
`context must be at most ${MAX_CONTEXT_BYTES} bytes (FIPS 204 section 5.2), got ${bytes.length}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Empty context is identical to no context under FIPS 204, so collapse it
|
|
67
|
+
// and keep the legacy call path byte-for-byte.
|
|
68
|
+
return bytes.length === 0 ? undefined : bytes
|
|
69
|
+
}
|
package/src/ml-dsa.d.ts
CHANGED
|
@@ -18,23 +18,56 @@ export function keypairFromMaster(
|
|
|
18
18
|
info?: string,
|
|
19
19
|
): MlDsaKeypair
|
|
20
20
|
|
|
21
|
+
/** Maximum context length in bytes (FIPS 204 section 5.2). */
|
|
22
|
+
export const MAX_CONTEXT_BYTES: 255
|
|
23
|
+
|
|
24
|
+
export interface SignatureOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Optional FIPS 204 section 5.2 context string, at most 255 bytes.
|
|
27
|
+
* Strings are encoded as UTF-8.
|
|
28
|
+
*
|
|
29
|
+
* Gives domain separation at the signature level: a signature made under a
|
|
30
|
+
* context does not verify without it, or under a different one. An empty
|
|
31
|
+
* context is identical to omitting it.
|
|
32
|
+
*
|
|
33
|
+
* Complements `keypairFromMaster(master, info)`, which separates domains at
|
|
34
|
+
* the key level.
|
|
35
|
+
*/
|
|
36
|
+
context?: Buffer | Uint8Array | string
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/**
|
|
22
40
|
* Sign a message under an ML-DSA-65 secret key. Returns the signature
|
|
23
41
|
* as a hex string (3309 bytes = 6618 hex characters).
|
|
42
|
+
*
|
|
43
|
+
* @throws {TypeError} if `opts` is not an options object, or `context` is
|
|
44
|
+
* neither a string nor a Uint8Array
|
|
45
|
+
* @throws {RangeError} if `context` exceeds 255 bytes
|
|
24
46
|
*/
|
|
25
47
|
export function sign(
|
|
26
48
|
secretKey: Buffer | Uint8Array,
|
|
27
|
-
message: Buffer | string,
|
|
49
|
+
message: Buffer | Uint8Array | string,
|
|
50
|
+
opts?: SignatureOptions,
|
|
28
51
|
): string
|
|
29
52
|
|
|
30
53
|
/**
|
|
31
54
|
* Verify a hex-encoded ML-DSA-65 signature against a public key + message.
|
|
32
|
-
*
|
|
55
|
+
*
|
|
56
|
+
* Returns `false` on any cryptographic failure (invalid hex, wrong length,
|
|
57
|
+
* mismatch, or a missing/incorrect context).
|
|
58
|
+
*
|
|
59
|
+
* Throws only on caller misuse of `opts`, which is a programming error rather
|
|
60
|
+
* than a failed verification and is not swallowed.
|
|
61
|
+
*
|
|
62
|
+
* @throws {TypeError} if `opts` is not an options object, or `context` is
|
|
63
|
+
* neither a string nor a Uint8Array
|
|
64
|
+
* @throws {RangeError} if `context` exceeds 255 bytes
|
|
33
65
|
*/
|
|
34
66
|
export function verify(
|
|
35
67
|
publicKey: Buffer | Uint8Array,
|
|
36
|
-
message: Buffer | string,
|
|
68
|
+
message: Buffer | Uint8Array | string,
|
|
37
69
|
sigHex: string,
|
|
70
|
+
opts?: SignatureOptions,
|
|
38
71
|
): boolean
|
|
39
72
|
|
|
40
73
|
/**
|
package/src/ml-dsa.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js'
|
|
10
10
|
import { deriveSeed } from './derive.js'
|
|
11
|
+
import { normalizeContext, MAX_CONTEXT_BYTES } from './_context.js'
|
|
12
|
+
|
|
13
|
+
export { MAX_CONTEXT_BYTES }
|
|
11
14
|
|
|
12
15
|
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
13
16
|
const enc = new TextEncoder()
|
|
@@ -50,26 +53,47 @@ export function keypairFromMaster(master, info = 'ml-dsa-65-v1') {
|
|
|
50
53
|
/**
|
|
51
54
|
* Sign a message. Returns the signature as a hex string.
|
|
52
55
|
*
|
|
56
|
+
* An optional FIPS 204 section 5.2 context string gives domain separation: a
|
|
57
|
+
* signature made under a context does not verify without it. Omit it and the
|
|
58
|
+
* behaviour is exactly as before this parameter existed.
|
|
59
|
+
*
|
|
53
60
|
* @param {Buffer|Uint8Array} secretKey
|
|
54
61
|
* @param {Buffer|Uint8Array|string} message
|
|
62
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts] at most 255 context bytes
|
|
55
63
|
* @returns {string} hex-encoded signature (6618 chars)
|
|
56
64
|
*/
|
|
57
|
-
export function sign(secretKey, message) {
|
|
58
|
-
const
|
|
65
|
+
export function sign(secretKey, message, opts) {
|
|
66
|
+
const context = normalizeContext(opts)
|
|
67
|
+
const sig = context === undefined
|
|
68
|
+
? ml_dsa65.sign(toBytes(message), secretKey)
|
|
69
|
+
: ml_dsa65.sign(toBytes(message), secretKey, { context })
|
|
59
70
|
return bytesToHex(sig)
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
/**
|
|
63
74
|
* Verify a hex-encoded signature.
|
|
64
75
|
*
|
|
76
|
+
* Pass the same context the signer used. A signature made under a context
|
|
77
|
+
* returns false here if the context is omitted or differs, which is the point
|
|
78
|
+
* of it.
|
|
79
|
+
*
|
|
80
|
+
* Returns false for any cryptographic failure. Throws only on caller misuse of
|
|
81
|
+
* `opts` (wrong type, or a context over 255 bytes), because that is a bug
|
|
82
|
+
* rather than a failed verification and should not be silently swallowed.
|
|
83
|
+
*
|
|
65
84
|
* @param {Buffer|Uint8Array} publicKey
|
|
66
85
|
* @param {Buffer|Uint8Array|string} message
|
|
67
86
|
* @param {string} sigHex
|
|
87
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts]
|
|
68
88
|
* @returns {boolean}
|
|
69
89
|
*/
|
|
70
|
-
export function verify(publicKey, message, sigHex) {
|
|
90
|
+
export function verify(publicKey, message, sigHex, opts) {
|
|
91
|
+
// Outside the try: misuse must surface, not be swallowed as "invalid".
|
|
92
|
+
const context = normalizeContext(opts)
|
|
71
93
|
try {
|
|
72
|
-
return
|
|
94
|
+
return context === undefined
|
|
95
|
+
? ml_dsa65.verify(hexToBytes(sigHex), toBytes(message), publicKey)
|
|
96
|
+
: ml_dsa65.verify(hexToBytes(sigHex), toBytes(message), publicKey, { context })
|
|
73
97
|
} catch {
|
|
74
98
|
return false
|
|
75
99
|
}
|
package/src/slh-dsa.d.ts
CHANGED
|
@@ -19,23 +19,49 @@ export function keypairFromMaster(
|
|
|
19
19
|
info?: string,
|
|
20
20
|
): SlhDsaKeypair
|
|
21
21
|
|
|
22
|
+
/** Maximum context length in bytes (FIPS 205, matching FIPS 204). */
|
|
23
|
+
export const MAX_CONTEXT_BYTES: 255
|
|
24
|
+
|
|
25
|
+
export interface SignatureOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Optional context string, at most 255 bytes. Strings are encoded as UTF-8.
|
|
28
|
+
* A signature made under a context does not verify without it. An empty
|
|
29
|
+
* context is identical to omitting it.
|
|
30
|
+
*/
|
|
31
|
+
context?: Buffer | Uint8Array | string
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
/**
|
|
23
35
|
* Sign a message under an SLH-DSA-SHA2-192s secret key. Returns the signature
|
|
24
36
|
* as a hex string (16224 bytes = 32448 hex characters).
|
|
37
|
+
*
|
|
38
|
+
* @throws {TypeError} if `opts` is not an options object, or `context` is
|
|
39
|
+
* neither a string nor a Uint8Array
|
|
40
|
+
* @throws {RangeError} if `context` exceeds 255 bytes
|
|
25
41
|
*/
|
|
26
42
|
export function sign(
|
|
27
43
|
secretKey: Buffer | Uint8Array,
|
|
28
|
-
message: Buffer | string,
|
|
44
|
+
message: Buffer | Uint8Array | string,
|
|
45
|
+
opts?: SignatureOptions,
|
|
29
46
|
): string
|
|
30
47
|
|
|
31
48
|
/**
|
|
32
49
|
* Verify a hex-encoded SLH-DSA-SHA2-192s signature against a public key +
|
|
33
|
-
* message.
|
|
50
|
+
* message.
|
|
51
|
+
*
|
|
52
|
+
* Returns `false` on any cryptographic failure (invalid hex, wrong length,
|
|
53
|
+
* mismatch, or a missing/incorrect context). Throws only on caller misuse of
|
|
54
|
+
* `opts`.
|
|
55
|
+
*
|
|
56
|
+
* @throws {TypeError} if `opts` is not an options object, or `context` is
|
|
57
|
+
* neither a string nor a Uint8Array
|
|
58
|
+
* @throws {RangeError} if `context` exceeds 255 bytes
|
|
34
59
|
*/
|
|
35
60
|
export function verify(
|
|
36
61
|
publicKey: Buffer | Uint8Array,
|
|
37
|
-
message: Buffer | string,
|
|
62
|
+
message: Buffer | Uint8Array | string,
|
|
38
63
|
sigHex: string,
|
|
64
|
+
opts?: SignatureOptions,
|
|
39
65
|
): boolean
|
|
40
66
|
|
|
41
67
|
/**
|
package/src/slh-dsa.js
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
import { slh_dsa_sha2_192s } from '@noble/post-quantum/slh-dsa.js'
|
|
17
17
|
import { deriveSeed } from './derive.js'
|
|
18
|
+
import { normalizeContext, MAX_CONTEXT_BYTES } from './_context.js'
|
|
19
|
+
|
|
20
|
+
export { MAX_CONTEXT_BYTES }
|
|
18
21
|
|
|
19
22
|
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
20
23
|
const enc = new TextEncoder()
|
|
@@ -60,26 +63,41 @@ export function keypairFromMaster(master, info = 'slh-dsa-sha2-192s-v1') {
|
|
|
60
63
|
/**
|
|
61
64
|
* Sign a message. Returns the signature as a hex string.
|
|
62
65
|
*
|
|
66
|
+
* Accepts the same optional context string as ml-dsa.js (FIPS 205 allows it on
|
|
67
|
+
* the same terms as FIPS 204, at most 255 bytes). Omit it and the behaviour is
|
|
68
|
+
* exactly as before this parameter existed.
|
|
69
|
+
*
|
|
63
70
|
* @param {Buffer|Uint8Array} secretKey
|
|
64
71
|
* @param {Buffer|Uint8Array|string} message
|
|
72
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts] at most 255 context bytes
|
|
65
73
|
* @returns {string} hex-encoded signature (32448 chars)
|
|
66
74
|
*/
|
|
67
|
-
export function sign(secretKey, message) {
|
|
68
|
-
const
|
|
75
|
+
export function sign(secretKey, message, opts) {
|
|
76
|
+
const context = normalizeContext(opts)
|
|
77
|
+
const sig = context === undefined
|
|
78
|
+
? slh_dsa_sha2_192s.sign(toBytes(message), secretKey)
|
|
79
|
+
: slh_dsa_sha2_192s.sign(toBytes(message), secretKey, { context })
|
|
69
80
|
return bytesToHex(sig)
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
/**
|
|
73
84
|
* Verify a hex-encoded signature.
|
|
74
85
|
*
|
|
86
|
+
* Pass the same context the signer used. Returns false for any cryptographic
|
|
87
|
+
* failure; throws only on caller misuse of `opts`.
|
|
88
|
+
*
|
|
75
89
|
* @param {Buffer|Uint8Array} publicKey
|
|
76
90
|
* @param {Buffer|Uint8Array|string} message
|
|
77
91
|
* @param {string} sigHex
|
|
92
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts]
|
|
78
93
|
* @returns {boolean}
|
|
79
94
|
*/
|
|
80
|
-
export function verify(publicKey, message, sigHex) {
|
|
95
|
+
export function verify(publicKey, message, sigHex, opts) {
|
|
96
|
+
const context = normalizeContext(opts)
|
|
81
97
|
try {
|
|
82
|
-
return
|
|
98
|
+
return context === undefined
|
|
99
|
+
? slh_dsa_sha2_192s.verify(hexToBytes(sigHex), toBytes(message), publicKey)
|
|
100
|
+
: slh_dsa_sha2_192s.verify(hexToBytes(sigHex), toBytes(message), publicKey, { context })
|
|
83
101
|
} catch {
|
|
84
102
|
return false
|
|
85
103
|
}
|