@7h3/protocol-pq 0.5.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/dist/aip-work/sdk/pq/src/index.d.ts +30 -0
- package/dist/aip-work/sdk/pq/src/index.js +100 -0
- package/dist/aip-work/src/protocol.d.ts +66 -0
- package/dist/aip-work/src/protocol.js +294 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +100 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +116 -0
- package/dist/sdk/pq/src/index.d.ts +30 -0
- package/dist/sdk/pq/src/index.js +100 -0
- package/dist/src/index.d.ts +30 -0
- package/dist/src/index.js +100 -0
- package/dist/src/protocol.d.ts +66 -0
- package/dist/src/protocol.js +294 -0
- package/package.json +42 -0
- package/src/index.test.ts +143 -0
- package/src/index.ts +166 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generatePqKeyPair, signEnvelopePq, verifyEnvelopePq, signEnvelopeMlDsa65, signEnvelopeMlDsa87, verifyEnvelopeMlDsa65, verifyEnvelopeMlDsa87, createEnvelope, } from './index.js';
|
|
3
|
+
// Conformance vector envelope (deterministic for tests)
|
|
4
|
+
const baseEnvelope = {
|
|
5
|
+
header: {
|
|
6
|
+
version: '7h3/0.1',
|
|
7
|
+
messageId: 'test-msg-001',
|
|
8
|
+
timestampMs: 1_700_000_000_000,
|
|
9
|
+
ttlMs: 60_000,
|
|
10
|
+
sender: 'agent-alpha',
|
|
11
|
+
recipient: 'agent-beta',
|
|
12
|
+
nonce: 'abc123xyz',
|
|
13
|
+
},
|
|
14
|
+
body: {
|
|
15
|
+
intent: 'TASK',
|
|
16
|
+
content: 'Hello, post-quantum world!',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
describe('generatePqKeyPair', () => {
|
|
20
|
+
it('1. ML-DSA-65 returns keypair with correct algorithm field', () => {
|
|
21
|
+
const kp = generatePqKeyPair('ML-DSA-65');
|
|
22
|
+
expect(kp.algorithm).toBe('ML-DSA-65');
|
|
23
|
+
expect(typeof kp.publicKey).toBe('string');
|
|
24
|
+
expect(typeof kp.privateKey).toBe('string');
|
|
25
|
+
expect(typeof kp.createdAt).toBe('number');
|
|
26
|
+
expect(kp.publicKey.length).toBeGreaterThan(0);
|
|
27
|
+
expect(kp.privateKey.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
it('2. ML-DSA-87 returns keypair', () => {
|
|
30
|
+
const kp = generatePqKeyPair('ML-DSA-87');
|
|
31
|
+
expect(kp.algorithm).toBe('ML-DSA-87');
|
|
32
|
+
expect(typeof kp.publicKey).toBe('string');
|
|
33
|
+
expect(typeof kp.privateKey).toBe('string');
|
|
34
|
+
});
|
|
35
|
+
it('7. ML-DSA-65 public key is correct size (1952 bytes → ~2603 base64url chars)', () => {
|
|
36
|
+
const kp = generatePqKeyPair('ML-DSA-65');
|
|
37
|
+
// base64url: ceil(1952 / 3) * 4 = 2604 chars, minus up to 2 padding = 2603 or 2604
|
|
38
|
+
const decoded = Buffer.from(kp.publicKey.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(kp.publicKey.length / 4) * 4, '='), 'base64');
|
|
39
|
+
expect(decoded.length).toBe(1952);
|
|
40
|
+
});
|
|
41
|
+
it('8. ML-DSA-65 private key is correct size (4032 bytes secretKey)', () => {
|
|
42
|
+
const kp = generatePqKeyPair('ML-DSA-65');
|
|
43
|
+
const decoded = Buffer.from(kp.privateKey.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(kp.privateKey.length / 4) * 4, '='), 'base64');
|
|
44
|
+
expect(decoded.length).toBe(4032);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('ML-DSA-65 sign/verify', () => {
|
|
48
|
+
it('3. ML-DSA-65 sign + verify round trip', async () => {
|
|
49
|
+
const kp = generatePqKeyPair('ML-DSA-65');
|
|
50
|
+
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-65');
|
|
51
|
+
expect(signed.signature?.alg).toBe('ML-DSA-65');
|
|
52
|
+
expect(typeof signed.signature?.value).toBe('string');
|
|
53
|
+
expect(signed.signature.value.length).toBeGreaterThan(0);
|
|
54
|
+
const valid = await verifyEnvelopePq(signed, kp.publicKey);
|
|
55
|
+
expect(valid).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('4. ML-DSA-65 verify fails on tampered envelope', async () => {
|
|
58
|
+
const kp = generatePqKeyPair('ML-DSA-65');
|
|
59
|
+
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-65');
|
|
60
|
+
const tampered = {
|
|
61
|
+
...signed,
|
|
62
|
+
body: { ...signed.body, content: 'TAMPERED content' },
|
|
63
|
+
};
|
|
64
|
+
const valid = await verifyEnvelopePq(tampered, kp.publicKey);
|
|
65
|
+
expect(valid).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('ML-DSA-87 sign/verify', () => {
|
|
69
|
+
it('5. ML-DSA-87 sign + verify round trip', async () => {
|
|
70
|
+
const kp = generatePqKeyPair('ML-DSA-87');
|
|
71
|
+
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-87');
|
|
72
|
+
expect(signed.signature?.alg).toBe('ML-DSA-87');
|
|
73
|
+
const valid = await verifyEnvelopePq(signed, kp.publicKey);
|
|
74
|
+
expect(valid).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it('6. ML-DSA-87 verify fails with wrong public key', async () => {
|
|
77
|
+
const kp = generatePqKeyPair('ML-DSA-87');
|
|
78
|
+
const wrongKp = generatePqKeyPair('ML-DSA-87');
|
|
79
|
+
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-87');
|
|
80
|
+
const valid = await verifyEnvelopePq(signed, wrongKp.publicKey);
|
|
81
|
+
expect(valid).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('algorithm-specific aliases', () => {
|
|
85
|
+
it('signEnvelopeMlDsa65 / verifyEnvelopeMlDsa65 round trip', async () => {
|
|
86
|
+
const kp = generatePqKeyPair('ML-DSA-65');
|
|
87
|
+
const envelope = createEnvelope({
|
|
88
|
+
sender: 'agent-a',
|
|
89
|
+
recipient: 'agent-b',
|
|
90
|
+
intent: 'PING',
|
|
91
|
+
content: 'alias test',
|
|
92
|
+
});
|
|
93
|
+
const signed = await signEnvelopeMlDsa65(envelope, kp.privateKey);
|
|
94
|
+
expect(signed.signature?.alg).toBe('ML-DSA-65');
|
|
95
|
+
const valid = await verifyEnvelopeMlDsa65(signed, kp.publicKey);
|
|
96
|
+
expect(valid).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it('signEnvelopeMlDsa87 / verifyEnvelopeMlDsa87 round trip', async () => {
|
|
99
|
+
const kp = generatePqKeyPair('ML-DSA-87');
|
|
100
|
+
const envelope = createEnvelope({
|
|
101
|
+
sender: 'agent-a',
|
|
102
|
+
recipient: 'agent-b',
|
|
103
|
+
intent: 'PONG',
|
|
104
|
+
content: 'ml-dsa-87 alias test',
|
|
105
|
+
});
|
|
106
|
+
const signed = await signEnvelopeMlDsa87(envelope, kp.privateKey);
|
|
107
|
+
expect(signed.signature?.alg).toBe('ML-DSA-87');
|
|
108
|
+
const valid = await verifyEnvelopeMlDsa87(signed, kp.publicKey);
|
|
109
|
+
expect(valid).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('verifyEnvelopePq returns false when no signature', async () => {
|
|
112
|
+
const unsigned = { ...baseEnvelope };
|
|
113
|
+
const valid = await verifyEnvelopePq(unsigned, 'fakepublickey');
|
|
114
|
+
expect(valid).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type { ProtocolEnvelope, ProtocolHeader, ProtocolBody } from '../../src/protocol.js';
|
|
2
|
+
export { canonicalizeEnvelope, createEnvelope } from '../../src/protocol.js';
|
|
3
|
+
import type { ProtocolEnvelope, ProtocolHeader, ProtocolBody } from '../../src/protocol.js';
|
|
4
|
+
export type PqAlgorithm = 'ML-DSA-65' | 'ML-DSA-87';
|
|
5
|
+
export interface PqKeyPair {
|
|
6
|
+
algorithm: PqAlgorithm;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
privateKey: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extended envelope type that carries a PQ signature.
|
|
13
|
+
* Structurally identical to ProtocolEnvelope but alg is widened.
|
|
14
|
+
*/
|
|
15
|
+
export interface PqProtocolEnvelope {
|
|
16
|
+
header: ProtocolHeader;
|
|
17
|
+
body: ProtocolBody;
|
|
18
|
+
signature?: {
|
|
19
|
+
alg: PqAlgorithm;
|
|
20
|
+
keyId: string;
|
|
21
|
+
value: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function generatePqKeyPair(algorithm?: PqAlgorithm): PqKeyPair;
|
|
25
|
+
export declare function signEnvelopePq(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyBase64Url: string, algorithm?: PqAlgorithm): Promise<PqProtocolEnvelope>;
|
|
26
|
+
export declare function verifyEnvelopePq(envelope: PqProtocolEnvelope, publicKeyBase64Url: string): Promise<boolean>;
|
|
27
|
+
export declare function signEnvelopeMlDsa65(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyBase64Url: string): Promise<PqProtocolEnvelope>;
|
|
28
|
+
export declare function signEnvelopeMlDsa87(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyBase64Url: string): Promise<PqProtocolEnvelope>;
|
|
29
|
+
export declare function verifyEnvelopeMlDsa65(envelope: PqProtocolEnvelope, publicKeyBase64Url: string): Promise<boolean>;
|
|
30
|
+
export declare function verifyEnvelopeMlDsa87(envelope: PqProtocolEnvelope, publicKeyBase64Url: string): Promise<boolean>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa';
|
|
2
|
+
export { canonicalizeEnvelope, createEnvelope } from '../../src/protocol.js';
|
|
3
|
+
import { canonicalizeEnvelope } from '../../src/protocol.js';
|
|
4
|
+
// ─── Base64url helpers ───────────────────────────────────────────────────────
|
|
5
|
+
function toBase64Url(bytes) {
|
|
6
|
+
const g = globalThis;
|
|
7
|
+
if (g.Buffer) {
|
|
8
|
+
return g.Buffer.from(bytes)
|
|
9
|
+
.toString('base64')
|
|
10
|
+
.replace(/\+/g, '-')
|
|
11
|
+
.replace(/\//g, '_')
|
|
12
|
+
.replace(/=+$/g, '');
|
|
13
|
+
}
|
|
14
|
+
let binary = '';
|
|
15
|
+
for (let i = 0; i < bytes.length; i++)
|
|
16
|
+
binary += String.fromCharCode(bytes[i]);
|
|
17
|
+
return btoa(binary)
|
|
18
|
+
.replace(/\+/g, '-')
|
|
19
|
+
.replace(/\//g, '_')
|
|
20
|
+
.replace(/=+$/g, '');
|
|
21
|
+
}
|
|
22
|
+
function fromBase64Url(value) {
|
|
23
|
+
const padded = value
|
|
24
|
+
.replace(/-/g, '+')
|
|
25
|
+
.replace(/_/g, '/')
|
|
26
|
+
.padEnd(Math.ceil(value.length / 4) * 4, '=');
|
|
27
|
+
const g = globalThis;
|
|
28
|
+
if (g.Buffer) {
|
|
29
|
+
return new Uint8Array(g.Buffer.from(padded, 'base64'));
|
|
30
|
+
}
|
|
31
|
+
const binary = atob(padded);
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i++)
|
|
34
|
+
bytes[i] = binary.charCodeAt(i);
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
// ─── Key generation ──────────────────────────────────────────────────────────
|
|
38
|
+
export function generatePqKeyPair(algorithm = 'ML-DSA-65') {
|
|
39
|
+
const impl = algorithm === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87;
|
|
40
|
+
const seed = crypto.getRandomValues(new Uint8Array(32));
|
|
41
|
+
const { publicKey, secretKey } = impl.keygen(seed);
|
|
42
|
+
return {
|
|
43
|
+
algorithm,
|
|
44
|
+
publicKey: toBase64Url(publicKey),
|
|
45
|
+
privateKey: toBase64Url(secretKey),
|
|
46
|
+
createdAt: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ─── Sign ────────────────────────────────────────────────────────────────────
|
|
50
|
+
export async function signEnvelopePq(envelope, privateKeyBase64Url, algorithm = 'ML-DSA-65') {
|
|
51
|
+
const impl = algorithm === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87;
|
|
52
|
+
const canonical = canonicalizeEnvelope(envelope);
|
|
53
|
+
const message = new TextEncoder().encode(canonical);
|
|
54
|
+
const secretKey = fromBase64Url(privateKeyBase64Url);
|
|
55
|
+
const sigBytes = impl.sign(secretKey, message);
|
|
56
|
+
// keyId derived from first 16 chars of the secretKey base64url
|
|
57
|
+
const keyId = privateKeyBase64Url.slice(0, 16);
|
|
58
|
+
return {
|
|
59
|
+
header: envelope.header,
|
|
60
|
+
body: envelope.body,
|
|
61
|
+
signature: {
|
|
62
|
+
alg: algorithm,
|
|
63
|
+
keyId,
|
|
64
|
+
value: toBase64Url(sigBytes),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ─── Verify ──────────────────────────────────────────────────────────────────
|
|
69
|
+
export async function verifyEnvelopePq(envelope, publicKeyBase64Url) {
|
|
70
|
+
if (!envelope.signature)
|
|
71
|
+
return false;
|
|
72
|
+
const alg = envelope.signature.alg;
|
|
73
|
+
if (alg !== 'ML-DSA-65' && alg !== 'ML-DSA-87')
|
|
74
|
+
return false;
|
|
75
|
+
const impl = alg === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87;
|
|
76
|
+
const unsigned = { header: envelope.header, body: envelope.body };
|
|
77
|
+
const canonical = canonicalizeEnvelope(unsigned);
|
|
78
|
+
const message = new TextEncoder().encode(canonical);
|
|
79
|
+
const publicKey = fromBase64Url(publicKeyBase64Url);
|
|
80
|
+
const sigBytes = fromBase64Url(envelope.signature.value);
|
|
81
|
+
try {
|
|
82
|
+
return impl.verify(publicKey, message, sigBytes);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ─── Algorithm-specific aliases ──────────────────────────────────────────────
|
|
89
|
+
export async function signEnvelopeMlDsa65(envelope, privateKeyBase64Url) {
|
|
90
|
+
return signEnvelopePq(envelope, privateKeyBase64Url, 'ML-DSA-65');
|
|
91
|
+
}
|
|
92
|
+
export async function signEnvelopeMlDsa87(envelope, privateKeyBase64Url) {
|
|
93
|
+
return signEnvelopePq(envelope, privateKeyBase64Url, 'ML-DSA-87');
|
|
94
|
+
}
|
|
95
|
+
export async function verifyEnvelopeMlDsa65(envelope, publicKeyBase64Url) {
|
|
96
|
+
return verifyEnvelopePq(envelope, publicKeyBase64Url);
|
|
97
|
+
}
|
|
98
|
+
export async function verifyEnvelopeMlDsa87(envelope, publicKeyBase64Url) {
|
|
99
|
+
return verifyEnvelopePq(envelope, publicKeyBase64Url);
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type { ProtocolEnvelope, ProtocolHeader, ProtocolBody } from '../../src/protocol.js';
|
|
2
|
+
export { canonicalizeEnvelope, createEnvelope } from '../../src/protocol.js';
|
|
3
|
+
import type { ProtocolEnvelope, ProtocolHeader, ProtocolBody } from '../../src/protocol.js';
|
|
4
|
+
export type PqAlgorithm = 'ML-DSA-65' | 'ML-DSA-87';
|
|
5
|
+
export interface PqKeyPair {
|
|
6
|
+
algorithm: PqAlgorithm;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
privateKey: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extended envelope type that carries a PQ signature.
|
|
13
|
+
* Structurally identical to ProtocolEnvelope but alg is widened.
|
|
14
|
+
*/
|
|
15
|
+
export interface PqProtocolEnvelope {
|
|
16
|
+
header: ProtocolHeader;
|
|
17
|
+
body: ProtocolBody;
|
|
18
|
+
signature?: {
|
|
19
|
+
alg: PqAlgorithm;
|
|
20
|
+
keyId: string;
|
|
21
|
+
value: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function generatePqKeyPair(algorithm?: PqAlgorithm): PqKeyPair;
|
|
25
|
+
export declare function signEnvelopePq(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyBase64Url: string, algorithm?: PqAlgorithm): Promise<PqProtocolEnvelope>;
|
|
26
|
+
export declare function verifyEnvelopePq(envelope: PqProtocolEnvelope, publicKeyBase64Url: string): Promise<boolean>;
|
|
27
|
+
export declare function signEnvelopeMlDsa65(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyBase64Url: string): Promise<PqProtocolEnvelope>;
|
|
28
|
+
export declare function signEnvelopeMlDsa87(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyBase64Url: string): Promise<PqProtocolEnvelope>;
|
|
29
|
+
export declare function verifyEnvelopeMlDsa65(envelope: PqProtocolEnvelope, publicKeyBase64Url: string): Promise<boolean>;
|
|
30
|
+
export declare function verifyEnvelopeMlDsa87(envelope: PqProtocolEnvelope, publicKeyBase64Url: string): Promise<boolean>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa';
|
|
2
|
+
export { canonicalizeEnvelope, createEnvelope } from '../../src/protocol.js';
|
|
3
|
+
import { canonicalizeEnvelope } from '../../src/protocol.js';
|
|
4
|
+
// ─── Base64url helpers ───────────────────────────────────────────────────────
|
|
5
|
+
function toBase64Url(bytes) {
|
|
6
|
+
const g = globalThis;
|
|
7
|
+
if (g.Buffer) {
|
|
8
|
+
return g.Buffer.from(bytes)
|
|
9
|
+
.toString('base64')
|
|
10
|
+
.replace(/\+/g, '-')
|
|
11
|
+
.replace(/\//g, '_')
|
|
12
|
+
.replace(/=+$/g, '');
|
|
13
|
+
}
|
|
14
|
+
let binary = '';
|
|
15
|
+
for (let i = 0; i < bytes.length; i++)
|
|
16
|
+
binary += String.fromCharCode(bytes[i]);
|
|
17
|
+
return btoa(binary)
|
|
18
|
+
.replace(/\+/g, '-')
|
|
19
|
+
.replace(/\//g, '_')
|
|
20
|
+
.replace(/=+$/g, '');
|
|
21
|
+
}
|
|
22
|
+
function fromBase64Url(value) {
|
|
23
|
+
const padded = value
|
|
24
|
+
.replace(/-/g, '+')
|
|
25
|
+
.replace(/_/g, '/')
|
|
26
|
+
.padEnd(Math.ceil(value.length / 4) * 4, '=');
|
|
27
|
+
const g = globalThis;
|
|
28
|
+
if (g.Buffer) {
|
|
29
|
+
return new Uint8Array(g.Buffer.from(padded, 'base64'));
|
|
30
|
+
}
|
|
31
|
+
const binary = atob(padded);
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i++)
|
|
34
|
+
bytes[i] = binary.charCodeAt(i);
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
// ─── Key generation ──────────────────────────────────────────────────────────
|
|
38
|
+
export function generatePqKeyPair(algorithm = 'ML-DSA-65') {
|
|
39
|
+
const impl = algorithm === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87;
|
|
40
|
+
const seed = crypto.getRandomValues(new Uint8Array(32));
|
|
41
|
+
const { publicKey, secretKey } = impl.keygen(seed);
|
|
42
|
+
return {
|
|
43
|
+
algorithm,
|
|
44
|
+
publicKey: toBase64Url(publicKey),
|
|
45
|
+
privateKey: toBase64Url(secretKey),
|
|
46
|
+
createdAt: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ─── Sign ────────────────────────────────────────────────────────────────────
|
|
50
|
+
export async function signEnvelopePq(envelope, privateKeyBase64Url, algorithm = 'ML-DSA-65') {
|
|
51
|
+
const impl = algorithm === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87;
|
|
52
|
+
const canonical = canonicalizeEnvelope(envelope);
|
|
53
|
+
const message = new TextEncoder().encode(canonical);
|
|
54
|
+
const secretKey = fromBase64Url(privateKeyBase64Url);
|
|
55
|
+
const sigBytes = impl.sign(secretKey, message);
|
|
56
|
+
// keyId derived from first 16 chars of the secretKey base64url
|
|
57
|
+
const keyId = privateKeyBase64Url.slice(0, 16);
|
|
58
|
+
return {
|
|
59
|
+
header: envelope.header,
|
|
60
|
+
body: envelope.body,
|
|
61
|
+
signature: {
|
|
62
|
+
alg: algorithm,
|
|
63
|
+
keyId,
|
|
64
|
+
value: toBase64Url(sigBytes),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ─── Verify ──────────────────────────────────────────────────────────────────
|
|
69
|
+
export async function verifyEnvelopePq(envelope, publicKeyBase64Url) {
|
|
70
|
+
if (!envelope.signature)
|
|
71
|
+
return false;
|
|
72
|
+
const alg = envelope.signature.alg;
|
|
73
|
+
if (alg !== 'ML-DSA-65' && alg !== 'ML-DSA-87')
|
|
74
|
+
return false;
|
|
75
|
+
const impl = alg === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87;
|
|
76
|
+
const unsigned = { header: envelope.header, body: envelope.body };
|
|
77
|
+
const canonical = canonicalizeEnvelope(unsigned);
|
|
78
|
+
const message = new TextEncoder().encode(canonical);
|
|
79
|
+
const publicKey = fromBase64Url(publicKeyBase64Url);
|
|
80
|
+
const sigBytes = fromBase64Url(envelope.signature.value);
|
|
81
|
+
try {
|
|
82
|
+
return impl.verify(publicKey, message, sigBytes);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ─── Algorithm-specific aliases ──────────────────────────────────────────────
|
|
89
|
+
export async function signEnvelopeMlDsa65(envelope, privateKeyBase64Url) {
|
|
90
|
+
return signEnvelopePq(envelope, privateKeyBase64Url, 'ML-DSA-65');
|
|
91
|
+
}
|
|
92
|
+
export async function signEnvelopeMlDsa87(envelope, privateKeyBase64Url) {
|
|
93
|
+
return signEnvelopePq(envelope, privateKeyBase64Url, 'ML-DSA-87');
|
|
94
|
+
}
|
|
95
|
+
export async function verifyEnvelopeMlDsa65(envelope, publicKeyBase64Url) {
|
|
96
|
+
return verifyEnvelopePq(envelope, publicKeyBase64Url);
|
|
97
|
+
}
|
|
98
|
+
export async function verifyEnvelopeMlDsa87(envelope, publicKeyBase64Url) {
|
|
99
|
+
return verifyEnvelopePq(envelope, publicKeyBase64Url);
|
|
100
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type ProtocolVersion = '7h3/0.1';
|
|
2
|
+
export type IntentKind = 'PING' | 'PONG' | 'CAPS' | 'TASK' | 'RESULT' | 'ERROR';
|
|
3
|
+
export interface ProtocolHeader {
|
|
4
|
+
version: ProtocolVersion;
|
|
5
|
+
messageId: string;
|
|
6
|
+
timestampMs: number;
|
|
7
|
+
ttlMs: number;
|
|
8
|
+
sender: string;
|
|
9
|
+
recipient?: string;
|
|
10
|
+
nonce: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProtocolBody {
|
|
13
|
+
intent: IntentKind;
|
|
14
|
+
content: string;
|
|
15
|
+
capability?: string;
|
|
16
|
+
correlationId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ProtocolSignature {
|
|
19
|
+
alg: 'HS256' | 'ED25519';
|
|
20
|
+
keyId: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ProtocolEnvelope {
|
|
24
|
+
header: ProtocolHeader;
|
|
25
|
+
body: ProtocolBody;
|
|
26
|
+
signature?: ProtocolSignature;
|
|
27
|
+
}
|
|
28
|
+
export interface ProtocolDiagnostic {
|
|
29
|
+
level: 'error' | 'warning';
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
export type SignatureVerificationMaterial = {
|
|
33
|
+
alg: 'HS256';
|
|
34
|
+
secret: string;
|
|
35
|
+
} | {
|
|
36
|
+
alg: 'ED25519';
|
|
37
|
+
publicKey: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function canonicalizeEnvelope(envelope: Omit<ProtocolEnvelope, 'signature'>): string;
|
|
40
|
+
export declare function signCanonicalPayloadHmac(payload: string, secret: string): Promise<string>;
|
|
41
|
+
export declare function verifyCanonicalPayloadHmac(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
42
|
+
export declare function generateEd25519KeypairBase64Url(): Promise<{
|
|
43
|
+
publicKey: string;
|
|
44
|
+
privateKey: string;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function signCanonicalPayloadEd25519(payload: string, privateKeyPkcs8Base64Url: string): Promise<string>;
|
|
47
|
+
export declare function verifyCanonicalPayloadEd25519(payload: string, signature: string, publicKeySpkiBase64Url: string): Promise<boolean>;
|
|
48
|
+
export declare function signEnvelopeHmac(envelope: Omit<ProtocolEnvelope, 'signature'>, secret: string, keyId?: string): Promise<ProtocolEnvelope>;
|
|
49
|
+
export declare function verifyEnvelopeHmac(envelope: ProtocolEnvelope, secret: string): Promise<boolean>;
|
|
50
|
+
export declare function signEnvelopeEd25519(envelope: Omit<ProtocolEnvelope, 'signature'>, privateKeyPkcs8Base64Url: string, keyId?: string): Promise<ProtocolEnvelope>;
|
|
51
|
+
export declare function verifyEnvelopeEd25519(envelope: ProtocolEnvelope, publicKeySpkiBase64Url: string): Promise<boolean>;
|
|
52
|
+
export declare function verifyEnvelopeSignature(envelope: ProtocolEnvelope, material: SignatureVerificationMaterial): Promise<boolean>;
|
|
53
|
+
export declare function verifyCanonicalPayloadSignature(payload: string, signature: ProtocolSignature | undefined, material: SignatureVerificationMaterial): Promise<boolean>;
|
|
54
|
+
export declare function validateEnvelope(envelope: ProtocolEnvelope, nowMs?: number): ProtocolDiagnostic[];
|
|
55
|
+
export declare function createEnvelope(input: {
|
|
56
|
+
sender: string;
|
|
57
|
+
recipient?: string;
|
|
58
|
+
intent: IntentKind;
|
|
59
|
+
content: string;
|
|
60
|
+
capability?: string;
|
|
61
|
+
correlationId?: string;
|
|
62
|
+
ttlMs?: number;
|
|
63
|
+
messageId?: string;
|
|
64
|
+
nonce?: string;
|
|
65
|
+
nowMs?: number;
|
|
66
|
+
}): Omit<ProtocolEnvelope, 'signature'>;
|