kxco-post-quantum 1.3.0 → 1.4.1
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/BENCHMARKS.md +124 -0
- package/CHANGELOG.md +413 -292
- package/CONFORMANCE.md +211 -0
- package/LICENSE +202 -202
- package/MIGRATION.md +143 -0
- package/README.md +216 -178
- package/SECURITY.md +63 -41
- package/THREAT-MODEL.md +194 -0
- package/package.json +126 -106
- package/src/derive.d.ts +20 -20
- package/src/derive.js +47 -47
- package/src/index.d.ts +13 -8
- package/src/index.js +24 -17
- package/src/kid.d.ts +21 -21
- package/src/kid.js +53 -53
- package/src/ml-dsa-87.d.ts +81 -0
- package/src/ml-dsa-87.js +127 -0
- package/src/ml-dsa.d.ts +78 -78
- package/src/ml-dsa.js +102 -102
- package/src/ml-kem-1024.d.ts +59 -0
- package/src/ml-kem-1024.js +90 -0
- package/src/ml-kem.d.ts +49 -49
- package/src/ml-kem.js +61 -61
- package/src/slh-dsa.d.ts +72 -72
- package/src/slh-dsa.js +106 -106
- package/src/webhook.d.ts +119 -119
- package/src/webhook.js +135 -135
package/src/ml-kem.js
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
// ML-KEM-768 helpers (NIST FIPS 203, Kyber768).
|
|
2
|
-
//
|
|
3
|
-
// Module-lattice key encapsulation. Security Category 3 (≈ AES-192). Public
|
|
4
|
-
// key 1184 bytes, ciphertext 1088 bytes, shared secret 32 bytes. Resistant
|
|
5
|
-
// to attacks by quantum computers.
|
|
6
|
-
//
|
|
7
|
-
// Isomorphic: works in Node and modern browsers. Returns Buffer on Node
|
|
8
|
-
// (backwards compatible), Uint8Array in browsers.
|
|
9
|
-
|
|
10
|
-
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'
|
|
11
|
-
import { deriveSeed } from './derive.js'
|
|
12
|
-
|
|
13
|
-
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
14
|
-
|
|
15
|
-
function wrap(bytes) {
|
|
16
|
-
return HAS_BUFFER ? Buffer.from(bytes) : bytes
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Generate an ML-KEM-768 keypair from a master + domain-separation info.
|
|
21
|
-
*
|
|
22
|
-
* @returns {{ publicKey: Buffer|Uint8Array, secretKey: Buffer|Uint8Array }}
|
|
23
|
-
*/
|
|
24
|
-
export function keypairFromMaster(master, info = 'ml-kem-768-v1') {
|
|
25
|
-
const seed = deriveSeed(master, info, 64)
|
|
26
|
-
const seedU8 = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
|
|
27
|
-
const k = ml_kem768.keygen(seedU8)
|
|
28
|
-
return {
|
|
29
|
-
publicKey: wrap(k.publicKey),
|
|
30
|
-
secretKey: wrap(k.secretKey),
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Encapsulate a shared secret to the recipient's public key.
|
|
36
|
-
*
|
|
37
|
-
* @param {Buffer|Uint8Array} publicKey
|
|
38
|
-
* @returns {{ ciphertext: Buffer|Uint8Array, cipherText: Buffer|Uint8Array, sharedSecret: Buffer|Uint8Array }}
|
|
39
|
-
*/
|
|
40
|
-
export function encapsulate(publicKey) {
|
|
41
|
-
const r = ml_kem768.encapsulate(publicKey)
|
|
42
|
-
const ct = wrap(r.cipherText ?? r.ciphertext)
|
|
43
|
-
return {
|
|
44
|
-
ciphertext: ct,
|
|
45
|
-
cipherText: ct,
|
|
46
|
-
sharedSecret: wrap(r.sharedSecret),
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Decapsulate: recover the shared secret from a ciphertext using the secret key.
|
|
52
|
-
*
|
|
53
|
-
* @param {Buffer|Uint8Array} ciphertext
|
|
54
|
-
* @param {Buffer|Uint8Array} secretKey
|
|
55
|
-
* @returns {Buffer|Uint8Array} shared secret (32 bytes)
|
|
56
|
-
*/
|
|
57
|
-
export function decapsulate(ciphertext, secretKey) {
|
|
58
|
-
return wrap(ml_kem768.decapsulate(ciphertext, secretKey))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export { ml_kem768 }
|
|
1
|
+
// ML-KEM-768 helpers (NIST FIPS 203, Kyber768).
|
|
2
|
+
//
|
|
3
|
+
// Module-lattice key encapsulation. Security Category 3 (≈ AES-192). Public
|
|
4
|
+
// key 1184 bytes, ciphertext 1088 bytes, shared secret 32 bytes. Resistant
|
|
5
|
+
// to attacks by quantum computers.
|
|
6
|
+
//
|
|
7
|
+
// Isomorphic: works in Node and modern browsers. Returns Buffer on Node
|
|
8
|
+
// (backwards compatible), Uint8Array in browsers.
|
|
9
|
+
|
|
10
|
+
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'
|
|
11
|
+
import { deriveSeed } from './derive.js'
|
|
12
|
+
|
|
13
|
+
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
14
|
+
|
|
15
|
+
function wrap(bytes) {
|
|
16
|
+
return HAS_BUFFER ? Buffer.from(bytes) : bytes
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate an ML-KEM-768 keypair from a master + domain-separation info.
|
|
21
|
+
*
|
|
22
|
+
* @returns {{ publicKey: Buffer|Uint8Array, secretKey: Buffer|Uint8Array }}
|
|
23
|
+
*/
|
|
24
|
+
export function keypairFromMaster(master, info = 'ml-kem-768-v1') {
|
|
25
|
+
const seed = deriveSeed(master, info, 64)
|
|
26
|
+
const seedU8 = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
|
|
27
|
+
const k = ml_kem768.keygen(seedU8)
|
|
28
|
+
return {
|
|
29
|
+
publicKey: wrap(k.publicKey),
|
|
30
|
+
secretKey: wrap(k.secretKey),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Encapsulate a shared secret to the recipient's public key.
|
|
36
|
+
*
|
|
37
|
+
* @param {Buffer|Uint8Array} publicKey
|
|
38
|
+
* @returns {{ ciphertext: Buffer|Uint8Array, cipherText: Buffer|Uint8Array, sharedSecret: Buffer|Uint8Array }}
|
|
39
|
+
*/
|
|
40
|
+
export function encapsulate(publicKey) {
|
|
41
|
+
const r = ml_kem768.encapsulate(publicKey)
|
|
42
|
+
const ct = wrap(r.cipherText ?? r.ciphertext)
|
|
43
|
+
return {
|
|
44
|
+
ciphertext: ct,
|
|
45
|
+
cipherText: ct,
|
|
46
|
+
sharedSecret: wrap(r.sharedSecret),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decapsulate: recover the shared secret from a ciphertext using the secret key.
|
|
52
|
+
*
|
|
53
|
+
* @param {Buffer|Uint8Array} ciphertext
|
|
54
|
+
* @param {Buffer|Uint8Array} secretKey
|
|
55
|
+
* @returns {Buffer|Uint8Array} shared secret (32 bytes)
|
|
56
|
+
*/
|
|
57
|
+
export function decapsulate(ciphertext, secretKey) {
|
|
58
|
+
return wrap(ml_kem768.decapsulate(ciphertext, secretKey))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { ml_kem768 }
|
package/src/slh-dsa.d.ts
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
export interface SlhDsaKeypair {
|
|
4
|
-
/** 48-byte public key */
|
|
5
|
-
publicKey: Buffer
|
|
6
|
-
/** 96-byte secret key */
|
|
7
|
-
secretKey: Buffer
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generate an SLH-DSA-SHA2-192s (NIST FIPS 205) keypair deterministically
|
|
12
|
-
* from a master + domain-separation info string.
|
|
13
|
-
*
|
|
14
|
-
* Same inputs always produce the same keypair — no state, no DB row.
|
|
15
|
-
* Security Category 3, matching ML-DSA-65.
|
|
16
|
-
*/
|
|
17
|
-
export function keypairFromMaster(
|
|
18
|
-
master: Buffer | Uint8Array,
|
|
19
|
-
info?: string,
|
|
20
|
-
): SlhDsaKeypair
|
|
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
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Sign a message under an SLH-DSA-SHA2-192s secret key. Returns the signature
|
|
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
|
|
41
|
-
*/
|
|
42
|
-
export function sign(
|
|
43
|
-
secretKey: Buffer | Uint8Array,
|
|
44
|
-
message: Buffer | Uint8Array | string,
|
|
45
|
-
opts?: SignatureOptions,
|
|
46
|
-
): string
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Verify a hex-encoded SLH-DSA-SHA2-192s signature against a public key +
|
|
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
|
|
59
|
-
*/
|
|
60
|
-
export function verify(
|
|
61
|
-
publicKey: Buffer | Uint8Array,
|
|
62
|
-
message: Buffer | Uint8Array | string,
|
|
63
|
-
sigHex: string,
|
|
64
|
-
opts?: SignatureOptions,
|
|
65
|
-
): boolean
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Raw `@noble/post-quantum` SLH-DSA-SHA2-192s primitive, re-exported for
|
|
69
|
-
* callers who want the lower-level API. The wrapper functions above are
|
|
70
|
-
* recommended for production use.
|
|
71
|
-
*/
|
|
72
|
-
export const slh_dsa_sha2_192s: typeof import('@noble/post-quantum/slh-dsa.js').slh_dsa_sha2_192s
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
export interface SlhDsaKeypair {
|
|
4
|
+
/** 48-byte public key */
|
|
5
|
+
publicKey: Buffer
|
|
6
|
+
/** 96-byte secret key */
|
|
7
|
+
secretKey: Buffer
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate an SLH-DSA-SHA2-192s (NIST FIPS 205) keypair deterministically
|
|
12
|
+
* from a master + domain-separation info string.
|
|
13
|
+
*
|
|
14
|
+
* Same inputs always produce the same keypair — no state, no DB row.
|
|
15
|
+
* Security Category 3, matching ML-DSA-65.
|
|
16
|
+
*/
|
|
17
|
+
export function keypairFromMaster(
|
|
18
|
+
master: Buffer | Uint8Array,
|
|
19
|
+
info?: string,
|
|
20
|
+
): SlhDsaKeypair
|
|
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
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sign a message under an SLH-DSA-SHA2-192s secret key. Returns the signature
|
|
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
|
|
41
|
+
*/
|
|
42
|
+
export function sign(
|
|
43
|
+
secretKey: Buffer | Uint8Array,
|
|
44
|
+
message: Buffer | Uint8Array | string,
|
|
45
|
+
opts?: SignatureOptions,
|
|
46
|
+
): string
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Verify a hex-encoded SLH-DSA-SHA2-192s signature against a public key +
|
|
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
|
|
59
|
+
*/
|
|
60
|
+
export function verify(
|
|
61
|
+
publicKey: Buffer | Uint8Array,
|
|
62
|
+
message: Buffer | Uint8Array | string,
|
|
63
|
+
sigHex: string,
|
|
64
|
+
opts?: SignatureOptions,
|
|
65
|
+
): boolean
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Raw `@noble/post-quantum` SLH-DSA-SHA2-192s primitive, re-exported for
|
|
69
|
+
* callers who want the lower-level API. The wrapper functions above are
|
|
70
|
+
* recommended for production use.
|
|
71
|
+
*/
|
|
72
|
+
export const slh_dsa_sha2_192s: typeof import('@noble/post-quantum/slh-dsa.js').slh_dsa_sha2_192s
|
package/src/slh-dsa.js
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
// SLH-DSA-SHA2-192s helpers (NIST FIPS 205, SPHINCS+).
|
|
2
|
-
//
|
|
3
|
-
// Stateless hash-based signatures. Security Category 3 (≈ AES-192), matching
|
|
4
|
-
// ML-DSA-65's security level. Public key 48 bytes, secret key 96 bytes,
|
|
5
|
-
// signature 16224 bytes. Security rests only on the SHA-2 hash function — no
|
|
6
|
-
// lattice or number-theoretic assumptions — which makes it the conservative
|
|
7
|
-
// hedge alongside ML-DSA-65.
|
|
8
|
-
//
|
|
9
|
-
// Tradeoff: signatures are ~5x larger than ML-DSA-65 (16224 vs 3309 bytes) and
|
|
10
|
-
// signing is slower. Use ML-DSA-65 as the default; reach for SLH-DSA when you
|
|
11
|
-
// want a signature whose security does not depend on lattice hardness.
|
|
12
|
-
//
|
|
13
|
-
// Isomorphic: works in Node and modern browsers. Returns Buffer on Node
|
|
14
|
-
// (consistent with ml-dsa.js), Uint8Array in browsers.
|
|
15
|
-
|
|
16
|
-
import { slh_dsa_sha2_192s } from '@noble/post-quantum/slh-dsa.js'
|
|
17
|
-
import { deriveSeed } from './derive.js'
|
|
18
|
-
import { normalizeContext, MAX_CONTEXT_BYTES } from './_context.js'
|
|
19
|
-
|
|
20
|
-
export { MAX_CONTEXT_BYTES }
|
|
21
|
-
|
|
22
|
-
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
23
|
-
const enc = new TextEncoder()
|
|
24
|
-
|
|
25
|
-
function toBytes(input) {
|
|
26
|
-
if (input instanceof Uint8Array) return input
|
|
27
|
-
if (typeof input === 'string') return enc.encode(input)
|
|
28
|
-
throw new Error('expected Uint8Array or string')
|
|
29
|
-
}
|
|
30
|
-
function hexToBytes(hex) {
|
|
31
|
-
if (typeof hex !== 'string' || hex.length % 2) throw new Error('invalid hex')
|
|
32
|
-
const b = new Uint8Array(hex.length / 2)
|
|
33
|
-
for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
|
34
|
-
return b
|
|
35
|
-
}
|
|
36
|
-
function bytesToHex(bytes) {
|
|
37
|
-
let s = ''
|
|
38
|
-
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0')
|
|
39
|
-
return s
|
|
40
|
-
}
|
|
41
|
-
function wrap(bytes) {
|
|
42
|
-
return HAS_BUFFER ? Buffer.from(bytes) : bytes
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Seed length for SLH-DSA-SHA2-192s keygen (FIPS 205: SK.seed || SK.prf || PK.seed).
|
|
46
|
-
const SEED_BYTES = slh_dsa_sha2_192s.lengths.seed
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Generate an SLH-DSA-SHA2-192s keypair from a master + domain-separation info.
|
|
50
|
-
*
|
|
51
|
-
* @returns {{ publicKey: Buffer|Uint8Array, secretKey: Buffer|Uint8Array }}
|
|
52
|
-
*/
|
|
53
|
-
export function keypairFromMaster(master, info = 'slh-dsa-sha2-192s-v1') {
|
|
54
|
-
const seed = deriveSeed(master, info, SEED_BYTES)
|
|
55
|
-
const seedU8 = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
|
|
56
|
-
const k = slh_dsa_sha2_192s.keygen(seedU8)
|
|
57
|
-
return {
|
|
58
|
-
publicKey: wrap(k.publicKey),
|
|
59
|
-
secretKey: wrap(k.secretKey),
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Sign a message. Returns the signature as a hex string.
|
|
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
|
-
*
|
|
70
|
-
* @param {Buffer|Uint8Array} secretKey
|
|
71
|
-
* @param {Buffer|Uint8Array|string} message
|
|
72
|
-
* @param {{ context?: Uint8Array|Buffer|string }} [opts] at most 255 context bytes
|
|
73
|
-
* @returns {string} hex-encoded signature (32448 chars)
|
|
74
|
-
*/
|
|
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 })
|
|
80
|
-
return bytesToHex(sig)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Verify a hex-encoded signature.
|
|
85
|
-
*
|
|
86
|
-
* Pass the same context the signer used. Returns false for any cryptographic
|
|
87
|
-
* failure; throws only on caller misuse of `opts`.
|
|
88
|
-
*
|
|
89
|
-
* @param {Buffer|Uint8Array} publicKey
|
|
90
|
-
* @param {Buffer|Uint8Array|string} message
|
|
91
|
-
* @param {string} sigHex
|
|
92
|
-
* @param {{ context?: Uint8Array|Buffer|string }} [opts]
|
|
93
|
-
* @returns {boolean}
|
|
94
|
-
*/
|
|
95
|
-
export function verify(publicKey, message, sigHex, opts) {
|
|
96
|
-
const context = normalizeContext(opts)
|
|
97
|
-
try {
|
|
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 })
|
|
101
|
-
} catch {
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export { slh_dsa_sha2_192s }
|
|
1
|
+
// SLH-DSA-SHA2-192s helpers (NIST FIPS 205, SPHINCS+).
|
|
2
|
+
//
|
|
3
|
+
// Stateless hash-based signatures. Security Category 3 (≈ AES-192), matching
|
|
4
|
+
// ML-DSA-65's security level. Public key 48 bytes, secret key 96 bytes,
|
|
5
|
+
// signature 16224 bytes. Security rests only on the SHA-2 hash function — no
|
|
6
|
+
// lattice or number-theoretic assumptions — which makes it the conservative
|
|
7
|
+
// hedge alongside ML-DSA-65.
|
|
8
|
+
//
|
|
9
|
+
// Tradeoff: signatures are ~5x larger than ML-DSA-65 (16224 vs 3309 bytes) and
|
|
10
|
+
// signing is slower. Use ML-DSA-65 as the default; reach for SLH-DSA when you
|
|
11
|
+
// want a signature whose security does not depend on lattice hardness.
|
|
12
|
+
//
|
|
13
|
+
// Isomorphic: works in Node and modern browsers. Returns Buffer on Node
|
|
14
|
+
// (consistent with ml-dsa.js), Uint8Array in browsers.
|
|
15
|
+
|
|
16
|
+
import { slh_dsa_sha2_192s } from '@noble/post-quantum/slh-dsa.js'
|
|
17
|
+
import { deriveSeed } from './derive.js'
|
|
18
|
+
import { normalizeContext, MAX_CONTEXT_BYTES } from './_context.js'
|
|
19
|
+
|
|
20
|
+
export { MAX_CONTEXT_BYTES }
|
|
21
|
+
|
|
22
|
+
const HAS_BUFFER = typeof Buffer !== 'undefined'
|
|
23
|
+
const enc = new TextEncoder()
|
|
24
|
+
|
|
25
|
+
function toBytes(input) {
|
|
26
|
+
if (input instanceof Uint8Array) return input
|
|
27
|
+
if (typeof input === 'string') return enc.encode(input)
|
|
28
|
+
throw new Error('expected Uint8Array or string')
|
|
29
|
+
}
|
|
30
|
+
function hexToBytes(hex) {
|
|
31
|
+
if (typeof hex !== 'string' || hex.length % 2) throw new Error('invalid hex')
|
|
32
|
+
const b = new Uint8Array(hex.length / 2)
|
|
33
|
+
for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
|
34
|
+
return b
|
|
35
|
+
}
|
|
36
|
+
function bytesToHex(bytes) {
|
|
37
|
+
let s = ''
|
|
38
|
+
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0')
|
|
39
|
+
return s
|
|
40
|
+
}
|
|
41
|
+
function wrap(bytes) {
|
|
42
|
+
return HAS_BUFFER ? Buffer.from(bytes) : bytes
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Seed length for SLH-DSA-SHA2-192s keygen (FIPS 205: SK.seed || SK.prf || PK.seed).
|
|
46
|
+
const SEED_BYTES = slh_dsa_sha2_192s.lengths.seed
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate an SLH-DSA-SHA2-192s keypair from a master + domain-separation info.
|
|
50
|
+
*
|
|
51
|
+
* @returns {{ publicKey: Buffer|Uint8Array, secretKey: Buffer|Uint8Array }}
|
|
52
|
+
*/
|
|
53
|
+
export function keypairFromMaster(master, info = 'slh-dsa-sha2-192s-v1') {
|
|
54
|
+
const seed = deriveSeed(master, info, SEED_BYTES)
|
|
55
|
+
const seedU8 = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
|
|
56
|
+
const k = slh_dsa_sha2_192s.keygen(seedU8)
|
|
57
|
+
return {
|
|
58
|
+
publicKey: wrap(k.publicKey),
|
|
59
|
+
secretKey: wrap(k.secretKey),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sign a message. Returns the signature as a hex string.
|
|
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
|
+
*
|
|
70
|
+
* @param {Buffer|Uint8Array} secretKey
|
|
71
|
+
* @param {Buffer|Uint8Array|string} message
|
|
72
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts] at most 255 context bytes
|
|
73
|
+
* @returns {string} hex-encoded signature (32448 chars)
|
|
74
|
+
*/
|
|
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 })
|
|
80
|
+
return bytesToHex(sig)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Verify a hex-encoded signature.
|
|
85
|
+
*
|
|
86
|
+
* Pass the same context the signer used. Returns false for any cryptographic
|
|
87
|
+
* failure; throws only on caller misuse of `opts`.
|
|
88
|
+
*
|
|
89
|
+
* @param {Buffer|Uint8Array} publicKey
|
|
90
|
+
* @param {Buffer|Uint8Array|string} message
|
|
91
|
+
* @param {string} sigHex
|
|
92
|
+
* @param {{ context?: Uint8Array|Buffer|string }} [opts]
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
export function verify(publicKey, message, sigHex, opts) {
|
|
96
|
+
const context = normalizeContext(opts)
|
|
97
|
+
try {
|
|
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 })
|
|
101
|
+
} catch {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { slh_dsa_sha2_192s }
|