@fabricorg/experiments-manifest-signing 0.0.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/LICENSE +21 -0
- package/dist/index.cjs +235 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +227 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabric
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/canonical.ts
|
|
4
|
+
function canonicalSignedPayload(m) {
|
|
5
|
+
return JSON.stringify(
|
|
6
|
+
{
|
|
7
|
+
contentHash: m.contentHash,
|
|
8
|
+
manifestVersion: m.manifestVersion,
|
|
9
|
+
schemaVersion: m.schemaVersion,
|
|
10
|
+
tenantId: m.tenantId
|
|
11
|
+
},
|
|
12
|
+
null,
|
|
13
|
+
0
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
function canonicalSignedBytes(m) {
|
|
17
|
+
return new TextEncoder().encode(canonicalSignedPayload(m));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/hash.ts
|
|
21
|
+
async function sha256(data) {
|
|
22
|
+
const subtle = await getSubtle();
|
|
23
|
+
const buf = await subtle.digest("SHA-256", data);
|
|
24
|
+
return new Uint8Array(buf);
|
|
25
|
+
}
|
|
26
|
+
function hexEncode(bytes) {
|
|
27
|
+
let s = "";
|
|
28
|
+
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, "0");
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
async function getSubtle() {
|
|
32
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
33
|
+
const { webcrypto } = await import('crypto');
|
|
34
|
+
return webcrypto.subtle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/sign.ts
|
|
38
|
+
async function signManifest(manifest, key) {
|
|
39
|
+
const subtle = await getSubtle2();
|
|
40
|
+
const payload = canonicalSignedBytes(manifest);
|
|
41
|
+
const signedHash = await sha256(payload);
|
|
42
|
+
const pkcs8 = ed25519PrivateKeyToPkcs8(hexDecode(key.privateKeyHex));
|
|
43
|
+
const cryptoKey = await subtle.importKey(
|
|
44
|
+
"pkcs8",
|
|
45
|
+
pkcs8,
|
|
46
|
+
{ name: "Ed25519" },
|
|
47
|
+
false,
|
|
48
|
+
["sign"]
|
|
49
|
+
);
|
|
50
|
+
const sigBuf = await subtle.sign({ name: "Ed25519" }, cryptoKey, payload);
|
|
51
|
+
return {
|
|
52
|
+
keyId: key.keyId,
|
|
53
|
+
alg: "ed25519",
|
|
54
|
+
sig: hexEncode(new Uint8Array(sigBuf)),
|
|
55
|
+
signedHash: hexEncode(signedHash)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function getSubtle2() {
|
|
59
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
60
|
+
const { webcrypto } = await import('crypto');
|
|
61
|
+
return webcrypto.subtle;
|
|
62
|
+
}
|
|
63
|
+
function hexDecode(hex) {
|
|
64
|
+
if (hex.length % 2 !== 0) throw new Error("hexDecode: odd length");
|
|
65
|
+
const out = new Uint8Array(hex.length / 2);
|
|
66
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function ed25519PrivateKeyToPkcs8(seed) {
|
|
70
|
+
if (seed.length !== 32) throw new Error("ed25519 seed must be 32 bytes");
|
|
71
|
+
const prefix = new Uint8Array([
|
|
72
|
+
48,
|
|
73
|
+
46,
|
|
74
|
+
2,
|
|
75
|
+
1,
|
|
76
|
+
0,
|
|
77
|
+
48,
|
|
78
|
+
5,
|
|
79
|
+
6,
|
|
80
|
+
3,
|
|
81
|
+
43,
|
|
82
|
+
101,
|
|
83
|
+
112,
|
|
84
|
+
4,
|
|
85
|
+
34,
|
|
86
|
+
4,
|
|
87
|
+
32
|
|
88
|
+
]);
|
|
89
|
+
const out = new Uint8Array(prefix.length + 32);
|
|
90
|
+
out.set(prefix, 0);
|
|
91
|
+
out.set(seed, prefix.length);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/verify.ts
|
|
96
|
+
async function verifyManifest(manifest, options) {
|
|
97
|
+
if (manifest.signature.keyId !== options.expectedKeyId) {
|
|
98
|
+
return { ok: false, reason: "keyId-mismatch" };
|
|
99
|
+
}
|
|
100
|
+
const payload = canonicalSignedBytes(manifest);
|
|
101
|
+
const expectedHash = hexEncode(await sha256(payload));
|
|
102
|
+
if (expectedHash !== manifest.signature.signedHash) {
|
|
103
|
+
return { ok: false, reason: "signedHash-mismatch" };
|
|
104
|
+
}
|
|
105
|
+
const subtle = await getSubtle3();
|
|
106
|
+
const pubKey = await subtle.importKey(
|
|
107
|
+
"raw",
|
|
108
|
+
hexDecode2(options.publicKeyHex),
|
|
109
|
+
{ name: "Ed25519" },
|
|
110
|
+
false,
|
|
111
|
+
["verify"]
|
|
112
|
+
);
|
|
113
|
+
const valid = await subtle.verify(
|
|
114
|
+
{ name: "Ed25519" },
|
|
115
|
+
pubKey,
|
|
116
|
+
hexDecode2(manifest.signature.sig),
|
|
117
|
+
payload
|
|
118
|
+
);
|
|
119
|
+
return valid ? { ok: true } : { ok: false, reason: "sig-invalid" };
|
|
120
|
+
}
|
|
121
|
+
async function getSubtle3() {
|
|
122
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
123
|
+
const { webcrypto } = await import('crypto');
|
|
124
|
+
return webcrypto.subtle;
|
|
125
|
+
}
|
|
126
|
+
function hexDecode2(hex) {
|
|
127
|
+
if (hex.length % 2 !== 0) throw new Error("hexDecode: odd length");
|
|
128
|
+
const out = new Uint8Array(hex.length / 2);
|
|
129
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/keypair.ts
|
|
134
|
+
async function generateKeypair(keyId) {
|
|
135
|
+
const subtle = await getSubtle4();
|
|
136
|
+
const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
|
|
137
|
+
"sign",
|
|
138
|
+
"verify"
|
|
139
|
+
]);
|
|
140
|
+
const rawPub = new Uint8Array(await subtle.exportKey("raw", kp.publicKey));
|
|
141
|
+
const pkcs8Priv = new Uint8Array(await subtle.exportKey("pkcs8", kp.privateKey));
|
|
142
|
+
const seed = pkcs8Priv.slice(pkcs8Priv.length - 32);
|
|
143
|
+
return {
|
|
144
|
+
publicKeyHex: hexEncode(rawPub),
|
|
145
|
+
privateKeyHex: hexEncode(seed),
|
|
146
|
+
keyId
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function getSubtle4() {
|
|
150
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
151
|
+
const { webcrypto } = await import('crypto');
|
|
152
|
+
return webcrypto.subtle;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/jwks.ts
|
|
156
|
+
function jwksFromKeys(keys) {
|
|
157
|
+
return {
|
|
158
|
+
keys: keys.map((k) => ({
|
|
159
|
+
kid: k.keyId,
|
|
160
|
+
kty: "OKP",
|
|
161
|
+
crv: "Ed25519",
|
|
162
|
+
x: hexToBase64Url(k.publicKeyHex),
|
|
163
|
+
alg: "EdDSA",
|
|
164
|
+
use: "sig",
|
|
165
|
+
status: k.status
|
|
166
|
+
}))
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function verifyAgainstJwks(manifest, options) {
|
|
170
|
+
const sig = manifest.signature;
|
|
171
|
+
if (options.expectedKeyIds && !options.expectedKeyIds.includes(sig.keyId)) {
|
|
172
|
+
return { ok: false, reason: "pinned-key-mismatch" };
|
|
173
|
+
}
|
|
174
|
+
const entry = options.jwks.keys.find((k) => k.kid === sig.keyId);
|
|
175
|
+
if (!entry) return { ok: false, reason: "unknown-key" };
|
|
176
|
+
if (entry.status === "retired") return { ok: false, reason: "retired-key" };
|
|
177
|
+
if (options.maxAgeMs && manifest.builtAt) {
|
|
178
|
+
const age = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime() - new Date(manifest.builtAt).getTime();
|
|
179
|
+
if (age > options.maxAgeMs) return { ok: false, reason: "replay-window" };
|
|
180
|
+
}
|
|
181
|
+
const payload = canonicalSignedBytes(manifest);
|
|
182
|
+
const expectedHash = hexEncode(await sha256(payload));
|
|
183
|
+
if (expectedHash !== sig.signedHash) return { ok: false, reason: "signedHash-mismatch" };
|
|
184
|
+
const subtle = await getSubtle5();
|
|
185
|
+
const pubKey = await subtle.importKey(
|
|
186
|
+
"raw",
|
|
187
|
+
base64UrlToBytes(entry.x),
|
|
188
|
+
{ name: "Ed25519" },
|
|
189
|
+
false,
|
|
190
|
+
["verify"]
|
|
191
|
+
);
|
|
192
|
+
const valid = await subtle.verify(
|
|
193
|
+
{ name: "Ed25519" },
|
|
194
|
+
pubKey,
|
|
195
|
+
hexToBytes(sig.sig),
|
|
196
|
+
payload
|
|
197
|
+
);
|
|
198
|
+
return valid ? { ok: true } : { ok: false, reason: "sig-invalid" };
|
|
199
|
+
}
|
|
200
|
+
async function getSubtle5() {
|
|
201
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
202
|
+
const { webcrypto } = await import('crypto');
|
|
203
|
+
return webcrypto.subtle;
|
|
204
|
+
}
|
|
205
|
+
function hexToBytes(hex) {
|
|
206
|
+
const out = new Uint8Array(hex.length / 2);
|
|
207
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function hexToBase64Url(hex) {
|
|
211
|
+
return bytesToBase64Url(hexToBytes(hex));
|
|
212
|
+
}
|
|
213
|
+
function bytesToBase64Url(bytes) {
|
|
214
|
+
let s = "";
|
|
215
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
216
|
+
return btoa(s).replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
217
|
+
}
|
|
218
|
+
function base64UrlToBytes(s) {
|
|
219
|
+
const pad = "=".repeat((4 - s.length % 4) % 4);
|
|
220
|
+
const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
221
|
+
const binary = atob(b64);
|
|
222
|
+
const out = new Uint8Array(binary.length);
|
|
223
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
exports.canonicalSignedBytes = canonicalSignedBytes;
|
|
228
|
+
exports.canonicalSignedPayload = canonicalSignedPayload;
|
|
229
|
+
exports.generateKeypair = generateKeypair;
|
|
230
|
+
exports.jwksFromKeys = jwksFromKeys;
|
|
231
|
+
exports.signManifest = signManifest;
|
|
232
|
+
exports.verifyAgainstJwks = verifyAgainstJwks;
|
|
233
|
+
exports.verifyManifest = verifyManifest;
|
|
234
|
+
//# sourceMappingURL=index.cjs.map
|
|
235
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts","../src/hash.ts","../src/sign.ts","../src/verify.ts","../src/keypair.ts","../src/jwks.ts"],"names":["getSubtle","hexDecode"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { SignableManifest } from './types.js';\n\n/**\n * Canonical JSON of the signed payload — sorted keys, no whitespace.\n *\n * The payload is intentionally NOT the full manifest. It is the minimum that\n * binds a (manifestVersion, contentHash) pair to a tenant under a schema. See\n * ADR-0002.\n */\nexport function canonicalSignedPayload(m: SignableManifest): string {\n return JSON.stringify(\n {\n contentHash: m.contentHash,\n manifestVersion: m.manifestVersion,\n schemaVersion: m.schemaVersion,\n tenantId: m.tenantId,\n },\n null,\n 0,\n );\n}\n\nexport function canonicalSignedBytes(m: SignableManifest): Uint8Array {\n return new TextEncoder().encode(canonicalSignedPayload(m));\n}\n","export async function sha256(data: Uint8Array): Promise<Uint8Array> {\n const subtle = await getSubtle();\n const buf = await subtle.digest('SHA-256', data as BufferSource);\n return new Uint8Array(buf);\n}\n\nexport function hexEncode(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += bytes[i]!.toString(16).padStart(2, '0');\n return s;\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n","import { canonicalSignedBytes } from './canonical.js';\nimport { hexEncode, sha256 } from './hash.js';\nimport type { ManifestSignature, SignableManifest } from './types.js';\n\n/** Universal-runtime Ed25519 signer (Node + Edge + browsers via crypto.subtle). */\nexport interface SignKey {\n /** Raw 32-byte Ed25519 private key as hex. */\n privateKeyHex: string;\n /** Identifier for the key — included in the signature for rotation tracking. */\n keyId: string;\n}\n\nexport async function signManifest(\n manifest: SignableManifest,\n key: SignKey,\n): Promise<ManifestSignature> {\n const subtle = await getSubtle();\n const payload = canonicalSignedBytes(manifest);\n const signedHash = await sha256(payload);\n\n const pkcs8 = ed25519PrivateKeyToPkcs8(hexDecode(key.privateKeyHex));\n const cryptoKey = await subtle.importKey(\n 'pkcs8',\n pkcs8 as BufferSource,\n { name: 'Ed25519' },\n false,\n ['sign'],\n );\n const sigBuf = await subtle.sign({ name: 'Ed25519' }, cryptoKey, payload as BufferSource);\n return {\n keyId: key.keyId,\n alg: 'ed25519',\n sig: hexEncode(new Uint8Array(sigBuf)),\n signedHash: hexEncode(signedHash),\n };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\nfunction hexDecode(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('hexDecode: odd length');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\n/** Wrap a raw 32-byte Ed25519 seed into a minimal PKCS#8 envelope (RFC 8410). */\nfunction ed25519PrivateKeyToPkcs8(seed: Uint8Array): Uint8Array {\n if (seed.length !== 32) throw new Error('ed25519 seed must be 32 bytes');\n const prefix = new Uint8Array([\n 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,\n ]);\n const out = new Uint8Array(prefix.length + 32);\n out.set(prefix, 0);\n out.set(seed, prefix.length);\n return out;\n}\n","import { canonicalSignedBytes } from './canonical.js';\nimport { hexEncode, sha256 } from './hash.js';\nimport type { ManifestSignature, SignableManifest } from './types.js';\n\nexport interface VerifyOptions {\n /** Hex-encoded raw 32-byte Ed25519 public key. */\n publicKeyHex: string;\n /** Required keyId — caller pins this at SDK init time. */\n expectedKeyId: string;\n}\n\nexport type VerifyResult =\n | { ok: true }\n | { ok: false; reason: 'keyId-mismatch' | 'signedHash-mismatch' | 'sig-invalid' };\n\nexport async function verifyManifest(\n manifest: SignableManifest & { signature: ManifestSignature },\n options: VerifyOptions,\n): Promise<VerifyResult> {\n if (manifest.signature.keyId !== options.expectedKeyId) {\n return { ok: false, reason: 'keyId-mismatch' };\n }\n const payload = canonicalSignedBytes(manifest);\n const expectedHash = hexEncode(await sha256(payload));\n if (expectedHash !== manifest.signature.signedHash) {\n return { ok: false, reason: 'signedHash-mismatch' };\n }\n\n const subtle = await getSubtle();\n const pubKey = await subtle.importKey(\n 'raw',\n hexDecode(options.publicKeyHex) as BufferSource,\n { name: 'Ed25519' },\n false,\n ['verify'],\n );\n const valid = await subtle.verify(\n { name: 'Ed25519' },\n pubKey,\n hexDecode(manifest.signature.sig) as BufferSource,\n payload as BufferSource,\n );\n return valid ? { ok: true } : { ok: false, reason: 'sig-invalid' };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\nfunction hexDecode(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('hexDecode: odd length');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n","/**\n * Ed25519 keypair generation for `fx dev` and tests. Production keys are\n * generated offline and stored in a secrets manager.\n */\nimport { hexEncode } from './hash.js';\n\nexport interface Ed25519Keypair {\n publicKeyHex: string;\n privateKeyHex: string;\n keyId: string;\n}\n\nexport async function generateKeypair(keyId: string): Promise<Ed25519Keypair> {\n const subtle = await getSubtle();\n const kp = (await subtle.generateKey({ name: 'Ed25519' }, true, [\n 'sign',\n 'verify',\n ])) as CryptoKeyPair;\n const rawPub = new Uint8Array(await subtle.exportKey('raw', kp.publicKey));\n const pkcs8Priv = new Uint8Array(await subtle.exportKey('pkcs8', kp.privateKey));\n // PKCS#8 ed25519 = 16-byte prefix + 32-byte seed. Strip the prefix.\n const seed = pkcs8Priv.slice(pkcs8Priv.length - 32);\n return {\n publicKeyHex: hexEncode(rawPub),\n privateKeyHex: hexEncode(seed),\n keyId,\n };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n","import { canonicalSignedBytes } from './canonical.js';\nimport { hexEncode, sha256 } from './hash.js';\nimport type { ManifestSignature, SignableManifest } from './types.js';\n\n/**\n * JWKS-style key set for a single org. Each entry is an Ed25519 public key\n * with a kid that matches a `manifest_keys.key_id` in the control plane.\n */\nexport interface JwksEntry {\n kid: string;\n kty: 'OKP';\n crv: 'Ed25519';\n /** Base64url-encoded raw 32-byte public key. */\n x: string;\n alg: 'EdDSA';\n use: 'sig';\n /** Lifecycle status — extension on top of the JWKS standard. */\n status: 'active' | 'retiring' | 'retired';\n}\n\nexport interface Jwks {\n keys: JwksEntry[];\n}\n\nexport function jwksFromKeys(\n keys: readonly {\n keyId: string;\n publicKeyHex: string;\n status: 'active' | 'retiring' | 'retired';\n }[],\n): Jwks {\n return {\n keys: keys.map((k) => ({\n kid: k.keyId,\n kty: 'OKP',\n crv: 'Ed25519',\n x: hexToBase64Url(k.publicKeyHex),\n alg: 'EdDSA',\n use: 'sig',\n status: k.status,\n })),\n };\n}\n\nexport interface VerifyAgainstJwksOptions {\n jwks: Jwks;\n /** Optional pinning. If set, rejects keys whose kid is not in this list. */\n expectedKeyIds?: string[];\n /** Reject manifests older than this many milliseconds (replay window). */\n maxAgeMs?: number;\n /** Override clock for tests. */\n now?: () => Date;\n}\n\nexport type JwksVerifyResult =\n | { ok: true }\n | {\n ok: false;\n reason:\n | 'unknown-key'\n | 'retired-key'\n | 'pinned-key-mismatch'\n | 'signedHash-mismatch'\n | 'sig-invalid'\n | 'replay-window';\n };\n\n/**\n * Verify a signed manifest against a JWKS document. Replaces ADR-0002's\n * single-key verifier when SDKs run in M3+ hosted mode.\n */\nexport async function verifyAgainstJwks(\n manifest: SignableManifest & { signature: ManifestSignature; builtAt?: string },\n options: VerifyAgainstJwksOptions,\n): Promise<JwksVerifyResult> {\n const sig = manifest.signature;\n if (options.expectedKeyIds && !options.expectedKeyIds.includes(sig.keyId)) {\n return { ok: false, reason: 'pinned-key-mismatch' };\n }\n const entry = options.jwks.keys.find((k) => k.kid === sig.keyId);\n if (!entry) return { ok: false, reason: 'unknown-key' };\n if (entry.status === 'retired') return { ok: false, reason: 'retired-key' };\n\n if (options.maxAgeMs && manifest.builtAt) {\n const age =\n (options.now ?? (() => new Date()))().getTime() - new Date(manifest.builtAt).getTime();\n if (age > options.maxAgeMs) return { ok: false, reason: 'replay-window' };\n }\n\n const payload = canonicalSignedBytes(manifest);\n const expectedHash = hexEncode(await sha256(payload));\n if (expectedHash !== sig.signedHash) return { ok: false, reason: 'signedHash-mismatch' };\n\n const subtle = await getSubtle();\n const pubKey = await subtle.importKey(\n 'raw',\n base64UrlToBytes(entry.x) as BufferSource,\n { name: 'Ed25519' },\n false,\n ['verify'],\n );\n const valid = await subtle.verify(\n { name: 'Ed25519' },\n pubKey,\n hexToBytes(sig.sig) as BufferSource,\n payload as BufferSource,\n );\n return valid ? { ok: true } : { ok: false, reason: 'sig-invalid' };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nfunction hexToBase64Url(hex: string): string {\n return bytesToBase64Url(hexToBytes(hex));\n}\n\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!);\n return btoa(s).replace(/=+$/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\nfunction base64UrlToBytes(s: string): Uint8Array {\n const pad = '='.repeat((4 - (s.length % 4)) % 4);\n const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/');\n const binary = atob(b64);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-declared structurally to keep this package free of `@fabricorg/experiments-domain`
|
|
3
|
+
* and `@fabricorg/experiments-manifest` imports — see boundary rules.
|
|
4
|
+
*
|
|
5
|
+
* The fields below are the subset that participates in the signed payload.
|
|
6
|
+
* See docs/adr/0002-manifest-signing.md for the protocol.
|
|
7
|
+
*/
|
|
8
|
+
interface SignableManifest {
|
|
9
|
+
schemaVersion: number;
|
|
10
|
+
manifestVersion: number;
|
|
11
|
+
tenantId: string;
|
|
12
|
+
contentHash: string;
|
|
13
|
+
}
|
|
14
|
+
interface ManifestSignature {
|
|
15
|
+
keyId: string;
|
|
16
|
+
alg: 'ed25519';
|
|
17
|
+
/** Hex-encoded 64-byte Ed25519 signature. */
|
|
18
|
+
sig: string;
|
|
19
|
+
/** Hex-encoded SHA-256 of the canonical signed payload. */
|
|
20
|
+
signedHash: string;
|
|
21
|
+
}
|
|
22
|
+
interface SignedManifest extends SignableManifest {
|
|
23
|
+
signature: ManifestSignature;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical JSON of the signed payload — sorted keys, no whitespace.
|
|
28
|
+
*
|
|
29
|
+
* The payload is intentionally NOT the full manifest. It is the minimum that
|
|
30
|
+
* binds a (manifestVersion, contentHash) pair to a tenant under a schema. See
|
|
31
|
+
* ADR-0002.
|
|
32
|
+
*/
|
|
33
|
+
declare function canonicalSignedPayload(m: SignableManifest): string;
|
|
34
|
+
declare function canonicalSignedBytes(m: SignableManifest): Uint8Array;
|
|
35
|
+
|
|
36
|
+
/** Universal-runtime Ed25519 signer (Node + Edge + browsers via crypto.subtle). */
|
|
37
|
+
interface SignKey {
|
|
38
|
+
/** Raw 32-byte Ed25519 private key as hex. */
|
|
39
|
+
privateKeyHex: string;
|
|
40
|
+
/** Identifier for the key — included in the signature for rotation tracking. */
|
|
41
|
+
keyId: string;
|
|
42
|
+
}
|
|
43
|
+
declare function signManifest(manifest: SignableManifest, key: SignKey): Promise<ManifestSignature>;
|
|
44
|
+
|
|
45
|
+
interface VerifyOptions {
|
|
46
|
+
/** Hex-encoded raw 32-byte Ed25519 public key. */
|
|
47
|
+
publicKeyHex: string;
|
|
48
|
+
/** Required keyId — caller pins this at SDK init time. */
|
|
49
|
+
expectedKeyId: string;
|
|
50
|
+
}
|
|
51
|
+
type VerifyResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: 'keyId-mismatch' | 'signedHash-mismatch' | 'sig-invalid';
|
|
56
|
+
};
|
|
57
|
+
declare function verifyManifest(manifest: SignableManifest & {
|
|
58
|
+
signature: ManifestSignature;
|
|
59
|
+
}, options: VerifyOptions): Promise<VerifyResult>;
|
|
60
|
+
|
|
61
|
+
interface Ed25519Keypair {
|
|
62
|
+
publicKeyHex: string;
|
|
63
|
+
privateKeyHex: string;
|
|
64
|
+
keyId: string;
|
|
65
|
+
}
|
|
66
|
+
declare function generateKeypair(keyId: string): Promise<Ed25519Keypair>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* JWKS-style key set for a single org. Each entry is an Ed25519 public key
|
|
70
|
+
* with a kid that matches a `manifest_keys.key_id` in the control plane.
|
|
71
|
+
*/
|
|
72
|
+
interface JwksEntry {
|
|
73
|
+
kid: string;
|
|
74
|
+
kty: 'OKP';
|
|
75
|
+
crv: 'Ed25519';
|
|
76
|
+
/** Base64url-encoded raw 32-byte public key. */
|
|
77
|
+
x: string;
|
|
78
|
+
alg: 'EdDSA';
|
|
79
|
+
use: 'sig';
|
|
80
|
+
/** Lifecycle status — extension on top of the JWKS standard. */
|
|
81
|
+
status: 'active' | 'retiring' | 'retired';
|
|
82
|
+
}
|
|
83
|
+
interface Jwks {
|
|
84
|
+
keys: JwksEntry[];
|
|
85
|
+
}
|
|
86
|
+
declare function jwksFromKeys(keys: readonly {
|
|
87
|
+
keyId: string;
|
|
88
|
+
publicKeyHex: string;
|
|
89
|
+
status: 'active' | 'retiring' | 'retired';
|
|
90
|
+
}[]): Jwks;
|
|
91
|
+
interface VerifyAgainstJwksOptions {
|
|
92
|
+
jwks: Jwks;
|
|
93
|
+
/** Optional pinning. If set, rejects keys whose kid is not in this list. */
|
|
94
|
+
expectedKeyIds?: string[];
|
|
95
|
+
/** Reject manifests older than this many milliseconds (replay window). */
|
|
96
|
+
maxAgeMs?: number;
|
|
97
|
+
/** Override clock for tests. */
|
|
98
|
+
now?: () => Date;
|
|
99
|
+
}
|
|
100
|
+
type JwksVerifyResult = {
|
|
101
|
+
ok: true;
|
|
102
|
+
} | {
|
|
103
|
+
ok: false;
|
|
104
|
+
reason: 'unknown-key' | 'retired-key' | 'pinned-key-mismatch' | 'signedHash-mismatch' | 'sig-invalid' | 'replay-window';
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Verify a signed manifest against a JWKS document. Replaces ADR-0002's
|
|
108
|
+
* single-key verifier when SDKs run in M3+ hosted mode.
|
|
109
|
+
*/
|
|
110
|
+
declare function verifyAgainstJwks(manifest: SignableManifest & {
|
|
111
|
+
signature: ManifestSignature;
|
|
112
|
+
builtAt?: string;
|
|
113
|
+
}, options: VerifyAgainstJwksOptions): Promise<JwksVerifyResult>;
|
|
114
|
+
|
|
115
|
+
export { type Ed25519Keypair, type Jwks, type JwksEntry, type JwksVerifyResult, type ManifestSignature, type SignKey, type SignableManifest, type SignedManifest, type VerifyAgainstJwksOptions, type VerifyOptions, type VerifyResult, canonicalSignedBytes, canonicalSignedPayload, generateKeypair, jwksFromKeys, signManifest, verifyAgainstJwks, verifyManifest };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-declared structurally to keep this package free of `@fabricorg/experiments-domain`
|
|
3
|
+
* and `@fabricorg/experiments-manifest` imports — see boundary rules.
|
|
4
|
+
*
|
|
5
|
+
* The fields below are the subset that participates in the signed payload.
|
|
6
|
+
* See docs/adr/0002-manifest-signing.md for the protocol.
|
|
7
|
+
*/
|
|
8
|
+
interface SignableManifest {
|
|
9
|
+
schemaVersion: number;
|
|
10
|
+
manifestVersion: number;
|
|
11
|
+
tenantId: string;
|
|
12
|
+
contentHash: string;
|
|
13
|
+
}
|
|
14
|
+
interface ManifestSignature {
|
|
15
|
+
keyId: string;
|
|
16
|
+
alg: 'ed25519';
|
|
17
|
+
/** Hex-encoded 64-byte Ed25519 signature. */
|
|
18
|
+
sig: string;
|
|
19
|
+
/** Hex-encoded SHA-256 of the canonical signed payload. */
|
|
20
|
+
signedHash: string;
|
|
21
|
+
}
|
|
22
|
+
interface SignedManifest extends SignableManifest {
|
|
23
|
+
signature: ManifestSignature;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical JSON of the signed payload — sorted keys, no whitespace.
|
|
28
|
+
*
|
|
29
|
+
* The payload is intentionally NOT the full manifest. It is the minimum that
|
|
30
|
+
* binds a (manifestVersion, contentHash) pair to a tenant under a schema. See
|
|
31
|
+
* ADR-0002.
|
|
32
|
+
*/
|
|
33
|
+
declare function canonicalSignedPayload(m: SignableManifest): string;
|
|
34
|
+
declare function canonicalSignedBytes(m: SignableManifest): Uint8Array;
|
|
35
|
+
|
|
36
|
+
/** Universal-runtime Ed25519 signer (Node + Edge + browsers via crypto.subtle). */
|
|
37
|
+
interface SignKey {
|
|
38
|
+
/** Raw 32-byte Ed25519 private key as hex. */
|
|
39
|
+
privateKeyHex: string;
|
|
40
|
+
/** Identifier for the key — included in the signature for rotation tracking. */
|
|
41
|
+
keyId: string;
|
|
42
|
+
}
|
|
43
|
+
declare function signManifest(manifest: SignableManifest, key: SignKey): Promise<ManifestSignature>;
|
|
44
|
+
|
|
45
|
+
interface VerifyOptions {
|
|
46
|
+
/** Hex-encoded raw 32-byte Ed25519 public key. */
|
|
47
|
+
publicKeyHex: string;
|
|
48
|
+
/** Required keyId — caller pins this at SDK init time. */
|
|
49
|
+
expectedKeyId: string;
|
|
50
|
+
}
|
|
51
|
+
type VerifyResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: 'keyId-mismatch' | 'signedHash-mismatch' | 'sig-invalid';
|
|
56
|
+
};
|
|
57
|
+
declare function verifyManifest(manifest: SignableManifest & {
|
|
58
|
+
signature: ManifestSignature;
|
|
59
|
+
}, options: VerifyOptions): Promise<VerifyResult>;
|
|
60
|
+
|
|
61
|
+
interface Ed25519Keypair {
|
|
62
|
+
publicKeyHex: string;
|
|
63
|
+
privateKeyHex: string;
|
|
64
|
+
keyId: string;
|
|
65
|
+
}
|
|
66
|
+
declare function generateKeypair(keyId: string): Promise<Ed25519Keypair>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* JWKS-style key set for a single org. Each entry is an Ed25519 public key
|
|
70
|
+
* with a kid that matches a `manifest_keys.key_id` in the control plane.
|
|
71
|
+
*/
|
|
72
|
+
interface JwksEntry {
|
|
73
|
+
kid: string;
|
|
74
|
+
kty: 'OKP';
|
|
75
|
+
crv: 'Ed25519';
|
|
76
|
+
/** Base64url-encoded raw 32-byte public key. */
|
|
77
|
+
x: string;
|
|
78
|
+
alg: 'EdDSA';
|
|
79
|
+
use: 'sig';
|
|
80
|
+
/** Lifecycle status — extension on top of the JWKS standard. */
|
|
81
|
+
status: 'active' | 'retiring' | 'retired';
|
|
82
|
+
}
|
|
83
|
+
interface Jwks {
|
|
84
|
+
keys: JwksEntry[];
|
|
85
|
+
}
|
|
86
|
+
declare function jwksFromKeys(keys: readonly {
|
|
87
|
+
keyId: string;
|
|
88
|
+
publicKeyHex: string;
|
|
89
|
+
status: 'active' | 'retiring' | 'retired';
|
|
90
|
+
}[]): Jwks;
|
|
91
|
+
interface VerifyAgainstJwksOptions {
|
|
92
|
+
jwks: Jwks;
|
|
93
|
+
/** Optional pinning. If set, rejects keys whose kid is not in this list. */
|
|
94
|
+
expectedKeyIds?: string[];
|
|
95
|
+
/** Reject manifests older than this many milliseconds (replay window). */
|
|
96
|
+
maxAgeMs?: number;
|
|
97
|
+
/** Override clock for tests. */
|
|
98
|
+
now?: () => Date;
|
|
99
|
+
}
|
|
100
|
+
type JwksVerifyResult = {
|
|
101
|
+
ok: true;
|
|
102
|
+
} | {
|
|
103
|
+
ok: false;
|
|
104
|
+
reason: 'unknown-key' | 'retired-key' | 'pinned-key-mismatch' | 'signedHash-mismatch' | 'sig-invalid' | 'replay-window';
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Verify a signed manifest against a JWKS document. Replaces ADR-0002's
|
|
108
|
+
* single-key verifier when SDKs run in M3+ hosted mode.
|
|
109
|
+
*/
|
|
110
|
+
declare function verifyAgainstJwks(manifest: SignableManifest & {
|
|
111
|
+
signature: ManifestSignature;
|
|
112
|
+
builtAt?: string;
|
|
113
|
+
}, options: VerifyAgainstJwksOptions): Promise<JwksVerifyResult>;
|
|
114
|
+
|
|
115
|
+
export { type Ed25519Keypair, type Jwks, type JwksEntry, type JwksVerifyResult, type ManifestSignature, type SignKey, type SignableManifest, type SignedManifest, type VerifyAgainstJwksOptions, type VerifyOptions, type VerifyResult, canonicalSignedBytes, canonicalSignedPayload, generateKeypair, jwksFromKeys, signManifest, verifyAgainstJwks, verifyManifest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
function canonicalSignedPayload(m) {
|
|
3
|
+
return JSON.stringify(
|
|
4
|
+
{
|
|
5
|
+
contentHash: m.contentHash,
|
|
6
|
+
manifestVersion: m.manifestVersion,
|
|
7
|
+
schemaVersion: m.schemaVersion,
|
|
8
|
+
tenantId: m.tenantId
|
|
9
|
+
},
|
|
10
|
+
null,
|
|
11
|
+
0
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function canonicalSignedBytes(m) {
|
|
15
|
+
return new TextEncoder().encode(canonicalSignedPayload(m));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/hash.ts
|
|
19
|
+
async function sha256(data) {
|
|
20
|
+
const subtle = await getSubtle();
|
|
21
|
+
const buf = await subtle.digest("SHA-256", data);
|
|
22
|
+
return new Uint8Array(buf);
|
|
23
|
+
}
|
|
24
|
+
function hexEncode(bytes) {
|
|
25
|
+
let s = "";
|
|
26
|
+
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, "0");
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
async function getSubtle() {
|
|
30
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
31
|
+
const { webcrypto } = await import('crypto');
|
|
32
|
+
return webcrypto.subtle;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/sign.ts
|
|
36
|
+
async function signManifest(manifest, key) {
|
|
37
|
+
const subtle = await getSubtle2();
|
|
38
|
+
const payload = canonicalSignedBytes(manifest);
|
|
39
|
+
const signedHash = await sha256(payload);
|
|
40
|
+
const pkcs8 = ed25519PrivateKeyToPkcs8(hexDecode(key.privateKeyHex));
|
|
41
|
+
const cryptoKey = await subtle.importKey(
|
|
42
|
+
"pkcs8",
|
|
43
|
+
pkcs8,
|
|
44
|
+
{ name: "Ed25519" },
|
|
45
|
+
false,
|
|
46
|
+
["sign"]
|
|
47
|
+
);
|
|
48
|
+
const sigBuf = await subtle.sign({ name: "Ed25519" }, cryptoKey, payload);
|
|
49
|
+
return {
|
|
50
|
+
keyId: key.keyId,
|
|
51
|
+
alg: "ed25519",
|
|
52
|
+
sig: hexEncode(new Uint8Array(sigBuf)),
|
|
53
|
+
signedHash: hexEncode(signedHash)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function getSubtle2() {
|
|
57
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
58
|
+
const { webcrypto } = await import('crypto');
|
|
59
|
+
return webcrypto.subtle;
|
|
60
|
+
}
|
|
61
|
+
function hexDecode(hex) {
|
|
62
|
+
if (hex.length % 2 !== 0) throw new Error("hexDecode: odd length");
|
|
63
|
+
const out = new Uint8Array(hex.length / 2);
|
|
64
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function ed25519PrivateKeyToPkcs8(seed) {
|
|
68
|
+
if (seed.length !== 32) throw new Error("ed25519 seed must be 32 bytes");
|
|
69
|
+
const prefix = new Uint8Array([
|
|
70
|
+
48,
|
|
71
|
+
46,
|
|
72
|
+
2,
|
|
73
|
+
1,
|
|
74
|
+
0,
|
|
75
|
+
48,
|
|
76
|
+
5,
|
|
77
|
+
6,
|
|
78
|
+
3,
|
|
79
|
+
43,
|
|
80
|
+
101,
|
|
81
|
+
112,
|
|
82
|
+
4,
|
|
83
|
+
34,
|
|
84
|
+
4,
|
|
85
|
+
32
|
|
86
|
+
]);
|
|
87
|
+
const out = new Uint8Array(prefix.length + 32);
|
|
88
|
+
out.set(prefix, 0);
|
|
89
|
+
out.set(seed, prefix.length);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/verify.ts
|
|
94
|
+
async function verifyManifest(manifest, options) {
|
|
95
|
+
if (manifest.signature.keyId !== options.expectedKeyId) {
|
|
96
|
+
return { ok: false, reason: "keyId-mismatch" };
|
|
97
|
+
}
|
|
98
|
+
const payload = canonicalSignedBytes(manifest);
|
|
99
|
+
const expectedHash = hexEncode(await sha256(payload));
|
|
100
|
+
if (expectedHash !== manifest.signature.signedHash) {
|
|
101
|
+
return { ok: false, reason: "signedHash-mismatch" };
|
|
102
|
+
}
|
|
103
|
+
const subtle = await getSubtle3();
|
|
104
|
+
const pubKey = await subtle.importKey(
|
|
105
|
+
"raw",
|
|
106
|
+
hexDecode2(options.publicKeyHex),
|
|
107
|
+
{ name: "Ed25519" },
|
|
108
|
+
false,
|
|
109
|
+
["verify"]
|
|
110
|
+
);
|
|
111
|
+
const valid = await subtle.verify(
|
|
112
|
+
{ name: "Ed25519" },
|
|
113
|
+
pubKey,
|
|
114
|
+
hexDecode2(manifest.signature.sig),
|
|
115
|
+
payload
|
|
116
|
+
);
|
|
117
|
+
return valid ? { ok: true } : { ok: false, reason: "sig-invalid" };
|
|
118
|
+
}
|
|
119
|
+
async function getSubtle3() {
|
|
120
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
121
|
+
const { webcrypto } = await import('crypto');
|
|
122
|
+
return webcrypto.subtle;
|
|
123
|
+
}
|
|
124
|
+
function hexDecode2(hex) {
|
|
125
|
+
if (hex.length % 2 !== 0) throw new Error("hexDecode: odd length");
|
|
126
|
+
const out = new Uint8Array(hex.length / 2);
|
|
127
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/keypair.ts
|
|
132
|
+
async function generateKeypair(keyId) {
|
|
133
|
+
const subtle = await getSubtle4();
|
|
134
|
+
const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
|
|
135
|
+
"sign",
|
|
136
|
+
"verify"
|
|
137
|
+
]);
|
|
138
|
+
const rawPub = new Uint8Array(await subtle.exportKey("raw", kp.publicKey));
|
|
139
|
+
const pkcs8Priv = new Uint8Array(await subtle.exportKey("pkcs8", kp.privateKey));
|
|
140
|
+
const seed = pkcs8Priv.slice(pkcs8Priv.length - 32);
|
|
141
|
+
return {
|
|
142
|
+
publicKeyHex: hexEncode(rawPub),
|
|
143
|
+
privateKeyHex: hexEncode(seed),
|
|
144
|
+
keyId
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function getSubtle4() {
|
|
148
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
149
|
+
const { webcrypto } = await import('crypto');
|
|
150
|
+
return webcrypto.subtle;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/jwks.ts
|
|
154
|
+
function jwksFromKeys(keys) {
|
|
155
|
+
return {
|
|
156
|
+
keys: keys.map((k) => ({
|
|
157
|
+
kid: k.keyId,
|
|
158
|
+
kty: "OKP",
|
|
159
|
+
crv: "Ed25519",
|
|
160
|
+
x: hexToBase64Url(k.publicKeyHex),
|
|
161
|
+
alg: "EdDSA",
|
|
162
|
+
use: "sig",
|
|
163
|
+
status: k.status
|
|
164
|
+
}))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function verifyAgainstJwks(manifest, options) {
|
|
168
|
+
const sig = manifest.signature;
|
|
169
|
+
if (options.expectedKeyIds && !options.expectedKeyIds.includes(sig.keyId)) {
|
|
170
|
+
return { ok: false, reason: "pinned-key-mismatch" };
|
|
171
|
+
}
|
|
172
|
+
const entry = options.jwks.keys.find((k) => k.kid === sig.keyId);
|
|
173
|
+
if (!entry) return { ok: false, reason: "unknown-key" };
|
|
174
|
+
if (entry.status === "retired") return { ok: false, reason: "retired-key" };
|
|
175
|
+
if (options.maxAgeMs && manifest.builtAt) {
|
|
176
|
+
const age = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime() - new Date(manifest.builtAt).getTime();
|
|
177
|
+
if (age > options.maxAgeMs) return { ok: false, reason: "replay-window" };
|
|
178
|
+
}
|
|
179
|
+
const payload = canonicalSignedBytes(manifest);
|
|
180
|
+
const expectedHash = hexEncode(await sha256(payload));
|
|
181
|
+
if (expectedHash !== sig.signedHash) return { ok: false, reason: "signedHash-mismatch" };
|
|
182
|
+
const subtle = await getSubtle5();
|
|
183
|
+
const pubKey = await subtle.importKey(
|
|
184
|
+
"raw",
|
|
185
|
+
base64UrlToBytes(entry.x),
|
|
186
|
+
{ name: "Ed25519" },
|
|
187
|
+
false,
|
|
188
|
+
["verify"]
|
|
189
|
+
);
|
|
190
|
+
const valid = await subtle.verify(
|
|
191
|
+
{ name: "Ed25519" },
|
|
192
|
+
pubKey,
|
|
193
|
+
hexToBytes(sig.sig),
|
|
194
|
+
payload
|
|
195
|
+
);
|
|
196
|
+
return valid ? { ok: true } : { ok: false, reason: "sig-invalid" };
|
|
197
|
+
}
|
|
198
|
+
async function getSubtle5() {
|
|
199
|
+
if (typeof crypto !== "undefined" && crypto.subtle) return crypto.subtle;
|
|
200
|
+
const { webcrypto } = await import('crypto');
|
|
201
|
+
return webcrypto.subtle;
|
|
202
|
+
}
|
|
203
|
+
function hexToBytes(hex) {
|
|
204
|
+
const out = new Uint8Array(hex.length / 2);
|
|
205
|
+
for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function hexToBase64Url(hex) {
|
|
209
|
+
return bytesToBase64Url(hexToBytes(hex));
|
|
210
|
+
}
|
|
211
|
+
function bytesToBase64Url(bytes) {
|
|
212
|
+
let s = "";
|
|
213
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
214
|
+
return btoa(s).replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
215
|
+
}
|
|
216
|
+
function base64UrlToBytes(s) {
|
|
217
|
+
const pad = "=".repeat((4 - s.length % 4) % 4);
|
|
218
|
+
const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
219
|
+
const binary = atob(b64);
|
|
220
|
+
const out = new Uint8Array(binary.length);
|
|
221
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { canonicalSignedBytes, canonicalSignedPayload, generateKeypair, jwksFromKeys, signManifest, verifyAgainstJwks, verifyManifest };
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
227
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts","../src/hash.ts","../src/sign.ts","../src/verify.ts","../src/keypair.ts","../src/jwks.ts"],"names":["getSubtle","hexDecode"],"mappings":"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","file":"index.js","sourcesContent":["import type { SignableManifest } from './types.js';\n\n/**\n * Canonical JSON of the signed payload — sorted keys, no whitespace.\n *\n * The payload is intentionally NOT the full manifest. It is the minimum that\n * binds a (manifestVersion, contentHash) pair to a tenant under a schema. See\n * ADR-0002.\n */\nexport function canonicalSignedPayload(m: SignableManifest): string {\n return JSON.stringify(\n {\n contentHash: m.contentHash,\n manifestVersion: m.manifestVersion,\n schemaVersion: m.schemaVersion,\n tenantId: m.tenantId,\n },\n null,\n 0,\n );\n}\n\nexport function canonicalSignedBytes(m: SignableManifest): Uint8Array {\n return new TextEncoder().encode(canonicalSignedPayload(m));\n}\n","export async function sha256(data: Uint8Array): Promise<Uint8Array> {\n const subtle = await getSubtle();\n const buf = await subtle.digest('SHA-256', data as BufferSource);\n return new Uint8Array(buf);\n}\n\nexport function hexEncode(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += bytes[i]!.toString(16).padStart(2, '0');\n return s;\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n","import { canonicalSignedBytes } from './canonical.js';\nimport { hexEncode, sha256 } from './hash.js';\nimport type { ManifestSignature, SignableManifest } from './types.js';\n\n/** Universal-runtime Ed25519 signer (Node + Edge + browsers via crypto.subtle). */\nexport interface SignKey {\n /** Raw 32-byte Ed25519 private key as hex. */\n privateKeyHex: string;\n /** Identifier for the key — included in the signature for rotation tracking. */\n keyId: string;\n}\n\nexport async function signManifest(\n manifest: SignableManifest,\n key: SignKey,\n): Promise<ManifestSignature> {\n const subtle = await getSubtle();\n const payload = canonicalSignedBytes(manifest);\n const signedHash = await sha256(payload);\n\n const pkcs8 = ed25519PrivateKeyToPkcs8(hexDecode(key.privateKeyHex));\n const cryptoKey = await subtle.importKey(\n 'pkcs8',\n pkcs8 as BufferSource,\n { name: 'Ed25519' },\n false,\n ['sign'],\n );\n const sigBuf = await subtle.sign({ name: 'Ed25519' }, cryptoKey, payload as BufferSource);\n return {\n keyId: key.keyId,\n alg: 'ed25519',\n sig: hexEncode(new Uint8Array(sigBuf)),\n signedHash: hexEncode(signedHash),\n };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\nfunction hexDecode(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('hexDecode: odd length');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\n/** Wrap a raw 32-byte Ed25519 seed into a minimal PKCS#8 envelope (RFC 8410). */\nfunction ed25519PrivateKeyToPkcs8(seed: Uint8Array): Uint8Array {\n if (seed.length !== 32) throw new Error('ed25519 seed must be 32 bytes');\n const prefix = new Uint8Array([\n 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,\n ]);\n const out = new Uint8Array(prefix.length + 32);\n out.set(prefix, 0);\n out.set(seed, prefix.length);\n return out;\n}\n","import { canonicalSignedBytes } from './canonical.js';\nimport { hexEncode, sha256 } from './hash.js';\nimport type { ManifestSignature, SignableManifest } from './types.js';\n\nexport interface VerifyOptions {\n /** Hex-encoded raw 32-byte Ed25519 public key. */\n publicKeyHex: string;\n /** Required keyId — caller pins this at SDK init time. */\n expectedKeyId: string;\n}\n\nexport type VerifyResult =\n | { ok: true }\n | { ok: false; reason: 'keyId-mismatch' | 'signedHash-mismatch' | 'sig-invalid' };\n\nexport async function verifyManifest(\n manifest: SignableManifest & { signature: ManifestSignature },\n options: VerifyOptions,\n): Promise<VerifyResult> {\n if (manifest.signature.keyId !== options.expectedKeyId) {\n return { ok: false, reason: 'keyId-mismatch' };\n }\n const payload = canonicalSignedBytes(manifest);\n const expectedHash = hexEncode(await sha256(payload));\n if (expectedHash !== manifest.signature.signedHash) {\n return { ok: false, reason: 'signedHash-mismatch' };\n }\n\n const subtle = await getSubtle();\n const pubKey = await subtle.importKey(\n 'raw',\n hexDecode(options.publicKeyHex) as BufferSource,\n { name: 'Ed25519' },\n false,\n ['verify'],\n );\n const valid = await subtle.verify(\n { name: 'Ed25519' },\n pubKey,\n hexDecode(manifest.signature.sig) as BufferSource,\n payload as BufferSource,\n );\n return valid ? { ok: true } : { ok: false, reason: 'sig-invalid' };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\nfunction hexDecode(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('hexDecode: odd length');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n","/**\n * Ed25519 keypair generation for `fx dev` and tests. Production keys are\n * generated offline and stored in a secrets manager.\n */\nimport { hexEncode } from './hash.js';\n\nexport interface Ed25519Keypair {\n publicKeyHex: string;\n privateKeyHex: string;\n keyId: string;\n}\n\nexport async function generateKeypair(keyId: string): Promise<Ed25519Keypair> {\n const subtle = await getSubtle();\n const kp = (await subtle.generateKey({ name: 'Ed25519' }, true, [\n 'sign',\n 'verify',\n ])) as CryptoKeyPair;\n const rawPub = new Uint8Array(await subtle.exportKey('raw', kp.publicKey));\n const pkcs8Priv = new Uint8Array(await subtle.exportKey('pkcs8', kp.privateKey));\n // PKCS#8 ed25519 = 16-byte prefix + 32-byte seed. Strip the prefix.\n const seed = pkcs8Priv.slice(pkcs8Priv.length - 32);\n return {\n publicKeyHex: hexEncode(rawPub),\n privateKeyHex: hexEncode(seed),\n keyId,\n };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n","import { canonicalSignedBytes } from './canonical.js';\nimport { hexEncode, sha256 } from './hash.js';\nimport type { ManifestSignature, SignableManifest } from './types.js';\n\n/**\n * JWKS-style key set for a single org. Each entry is an Ed25519 public key\n * with a kid that matches a `manifest_keys.key_id` in the control plane.\n */\nexport interface JwksEntry {\n kid: string;\n kty: 'OKP';\n crv: 'Ed25519';\n /** Base64url-encoded raw 32-byte public key. */\n x: string;\n alg: 'EdDSA';\n use: 'sig';\n /** Lifecycle status — extension on top of the JWKS standard. */\n status: 'active' | 'retiring' | 'retired';\n}\n\nexport interface Jwks {\n keys: JwksEntry[];\n}\n\nexport function jwksFromKeys(\n keys: readonly {\n keyId: string;\n publicKeyHex: string;\n status: 'active' | 'retiring' | 'retired';\n }[],\n): Jwks {\n return {\n keys: keys.map((k) => ({\n kid: k.keyId,\n kty: 'OKP',\n crv: 'Ed25519',\n x: hexToBase64Url(k.publicKeyHex),\n alg: 'EdDSA',\n use: 'sig',\n status: k.status,\n })),\n };\n}\n\nexport interface VerifyAgainstJwksOptions {\n jwks: Jwks;\n /** Optional pinning. If set, rejects keys whose kid is not in this list. */\n expectedKeyIds?: string[];\n /** Reject manifests older than this many milliseconds (replay window). */\n maxAgeMs?: number;\n /** Override clock for tests. */\n now?: () => Date;\n}\n\nexport type JwksVerifyResult =\n | { ok: true }\n | {\n ok: false;\n reason:\n | 'unknown-key'\n | 'retired-key'\n | 'pinned-key-mismatch'\n | 'signedHash-mismatch'\n | 'sig-invalid'\n | 'replay-window';\n };\n\n/**\n * Verify a signed manifest against a JWKS document. Replaces ADR-0002's\n * single-key verifier when SDKs run in M3+ hosted mode.\n */\nexport async function verifyAgainstJwks(\n manifest: SignableManifest & { signature: ManifestSignature; builtAt?: string },\n options: VerifyAgainstJwksOptions,\n): Promise<JwksVerifyResult> {\n const sig = manifest.signature;\n if (options.expectedKeyIds && !options.expectedKeyIds.includes(sig.keyId)) {\n return { ok: false, reason: 'pinned-key-mismatch' };\n }\n const entry = options.jwks.keys.find((k) => k.kid === sig.keyId);\n if (!entry) return { ok: false, reason: 'unknown-key' };\n if (entry.status === 'retired') return { ok: false, reason: 'retired-key' };\n\n if (options.maxAgeMs && manifest.builtAt) {\n const age =\n (options.now ?? (() => new Date()))().getTime() - new Date(manifest.builtAt).getTime();\n if (age > options.maxAgeMs) return { ok: false, reason: 'replay-window' };\n }\n\n const payload = canonicalSignedBytes(manifest);\n const expectedHash = hexEncode(await sha256(payload));\n if (expectedHash !== sig.signedHash) return { ok: false, reason: 'signedHash-mismatch' };\n\n const subtle = await getSubtle();\n const pubKey = await subtle.importKey(\n 'raw',\n base64UrlToBytes(entry.x) as BufferSource,\n { name: 'Ed25519' },\n false,\n ['verify'],\n );\n const valid = await subtle.verify(\n { name: 'Ed25519' },\n pubKey,\n hexToBytes(sig.sig) as BufferSource,\n payload as BufferSource,\n );\n return valid ? { ok: true } : { ok: false, reason: 'sig-invalid' };\n}\n\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (typeof crypto !== 'undefined' && crypto.subtle) return crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nfunction hexToBase64Url(hex: string): string {\n return bytesToBase64Url(hexToBytes(hex));\n}\n\nfunction bytesToBase64Url(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!);\n return btoa(s).replace(/=+$/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\nfunction base64UrlToBytes(s: string): Uint8Array {\n const pad = '='.repeat((4 - (s.length % 4)) % 4);\n const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/');\n const binary = atob(b64);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fabricorg/experiments-manifest-signing",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Ed25519 sign + verify utilities for Fabric Experiments manifests. Works in Node, Edge, and browsers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.3.5",
|
|
28
|
+
"typescript": "^5.8.3",
|
|
29
|
+
"vitest": "^2.1.8"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"type-check": "tsc --noEmit",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"clean": "rm -rf dist"
|
|
36
|
+
}
|
|
37
|
+
}
|