-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest.js
More file actions
265 lines (238 loc) · 10.2 KB
/
test.js
File metadata and controls
265 lines (238 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import {
normalizeText,
buildSignatureBinding,
buildEndorsementBinding,
verifySignature,
didWebResolver,
directUrlResolver,
trustDirectoryResolver,
resolveKey,
verifyEndorsement,
} from './index.js';
import { generateKeyPairSync, sign as nodeSign, createHash } from 'node:crypto';
import { createServer } from 'node:http';
const tests = [
// [inputA, inputB, shouldMatch, description]
['\u201CHello\u201D', '"Hello"', true, 'Curly double quotes → straight'],
['caf\u00E9', 'cafe\u0301', true, 'Precomposed vs combining (NFKC)'],
['\uFB01nd', 'find', true, 'fi ligature (NFKC)'],
['word \u2014 word', 'word - word', true, 'Em dash → hyphen-minus'],
['\u00ABBonjour\u00BB', '"Bonjour"', true, 'Guillemets → double quotes'],
['\u300C\u6771\u4EAC\u300D', '"\u6771\u4EAC"', true, 'CJK corner brackets → double quotes'],
['\u0645\u06CC\u200C\u062E\u0648\u0627\u0647\u0645', '\u0645\u06CC\u062E\u0648\u0627\u0647\u0645', false, 'ZWNJ is semantic (Persian)'],
['\u0643\u062A\u0640\u0640\u0640\u0627\u0628', '\u0643\u062A\u0627\u0628', true, 'Arabic tatweel stripped'],
['\uFF21\uFF11', 'A1', true, 'Fullwidth ASCII (NFKC)'],
['\u2460', '1', true, 'Circled digit (NFKC)'],
['word\u200Bword', 'wordword', true, 'ZWSP stripped'],
['word\u200Cword', 'wordword', false, 'ZWNJ preserved (different)'],
['Hello\u2026', 'Hello...', true, 'Ellipsis → three dots'],
['\u2018Hello\u2019', "'Hello'", true, 'Curly single quotes → straight'],
['\u201AGerman\u201C', '"German"', true, 'Low-9 quotes → straight'],
['a\u00A0b', 'a b', true, 'No-break space → space'],
['a\u3000b', 'a b', true, 'Ideographic space → space'],
['a \t b', 'a b', true, 'Whitespace collapse'],
];
let passed = 0;
let failed = 0;
for (const [inputA, inputB, shouldMatch, desc] of tests) {
const a = normalizeText(inputA);
const b = normalizeText(inputB);
const match = a === b;
if (match === shouldMatch) {
passed++;
console.log(` ✓ ${desc}`);
} else {
failed++;
console.log(` ✗ ${desc}`);
console.log(` A: ${JSON.stringify(a)}`);
console.log(` B: ${JSON.stringify(b)}`);
console.log(` Expected ${shouldMatch ? 'same' : 'different'}, got ${match ? 'same' : 'different'}`);
}
}
// ============================================================================
// Async tests for signature binding, verification, resolvers, endorsements
// ============================================================================
async function check(name, fn) {
try {
await fn();
passed++;
console.log(` ✓ ${name}`);
} catch (err) {
failed++;
console.log(` ✗ ${name}`);
console.log(` ${err.message}`);
}
}
function assert(cond, msg) {
if (!cond) throw new Error(msg || 'assertion failed');
}
function assertEq(a, b, msg) {
if (a !== b) throw new Error(`${msg || 'not equal'}: ${JSON.stringify(a)} !== ${JSON.stringify(b)}`);
}
await check('buildSignatureBinding produces colon-joined string', () => {
const s = buildSignatureBinding({
contentHash: 'sha256:AAA',
claimsHash: 'sha256:BBB',
domain: 'example.org',
signedAt: '2026-04-28T00:00:00Z',
});
assertEq(s, 'sha256:AAA:sha256:BBB:example.org:2026-04-28T00:00:00Z');
});
await check('buildSignatureBinding throws on missing field', () => {
let threw = false;
try {
buildSignatureBinding({ contentHash: '', claimsHash: 'x', domain: 'd', signedAt: 't' });
} catch {
threw = true;
}
assert(threw, 'expected throw on missing field');
});
await check('verifySignature ed25519 round-trip', async () => {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const message = 'hello world';
const sig = nodeSign(null, Buffer.from(message), privateKey).toString('base64');
const pem = publicKey.export({ type: 'spki', format: 'pem' });
const ok = await verifySignature(message, sig, pem, 'ed25519');
assert(ok, 'expected ed25519 signature to verify');
const tampered = await verifySignature(message + '!', sig, pem, 'ed25519');
assert(!tampered, 'tampered message must not verify');
});
await check('verifySignature rsa round-trip', async () => {
const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const { createSign } = await import('node:crypto');
const signer = createSign('SHA256');
signer.update('hello rsa');
const sig = signer.sign(privateKey, 'base64');
const pem = publicKey.export({ type: 'spki', format: 'pem' });
const ok = await verifySignature('hello rsa', sig, pem, 'rsa');
assert(ok, 'expected rsa signature to verify');
});
await check('verifySignature handles unpadded base64', async () => {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const sig = nodeSign(null, Buffer.from('msg'), privateKey).toString('base64').replace(/=+$/, '');
const pem = publicKey.export({ type: 'spki', format: 'pem' });
const ok = await verifySignature('msg', sig, pem, 'ED25519');
assert(ok, 'unpadded base64 should still verify');
});
// ---- Resolver tests with a local HTTP fixture ----
function startFixtureServer(routes) {
return new Promise((resolve) => {
const server = createServer((req, res) => {
const handler = routes[req.url];
if (!handler) {
res.writeHead(404);
res.end();
return;
}
const r = handler();
res.writeHead(r.status || 200, r.headers || { 'content-type': 'application/json' });
res.end(typeof r.body === 'string' ? r.body : JSON.stringify(r.body));
});
server.listen(0, '127.0.0.1', () => resolve(server));
});
}
const { publicKey: edPub } = generateKeyPairSync('ed25519');
const edPubPem = edPub.export({ type: 'spki', format: 'pem' });
const fixtureServer = await startFixtureServer({
'/.well-known/did.json': () => ({
body: {
id: 'did:web:127.0.0.1',
verificationMethod: [
{ id: '#key-1', type: 'Ed25519VerificationKey2020', publicKeyPem: edPubPem },
],
},
}),
'/key.json': () => ({ body: { publicKey: edPubPem, algorithm: 'ed25519' } }),
'/keys/abc123': () => ({ body: { publicKey: edPubPem, algorithm: 'ed25519' } }),
});
const port = fixtureServer.address().port;
const base = `http://127.0.0.1:${port}`;
await check('didWebResolver fetches did.json and extracts key', async () => {
// Override scheme to http for the fixture
const r = {
async resolve(keyid) {
if (!keyid?.startsWith('did:web:')) return null;
const rest = keyid.slice('did:web:'.length);
const url = `http://${rest}/.well-known/did.json`;
const res = await fetch(url);
const doc = await res.json();
const vm = (doc.verificationMethod || []).find((m) => m.publicKeyPem);
return vm ? { keyid, publicKeyPem: vm.publicKeyPem, algorithm: 'ed25519' } : null;
},
};
// Use the real resolver against a live https fixture would need TLS; we
// already cover http path via directUrlResolver. Here we exercise the
// shape contract via the same code path through resolveKey().
const resolved = await resolveKey(`did:web:127.0.0.1:${port}`, [r]);
assert(resolved, 'expected did:web resolver to resolve');
assert(resolved.publicKeyPem.includes('BEGIN PUBLIC KEY'), 'expected PEM');
});
await check('directUrlResolver fetches http URL keyid', async () => {
const resolved = await resolveKey(`${base}/key.json`, [directUrlResolver()]);
assert(resolved, 'expected resolution');
assertEq(resolved.algorithm, 'ed25519');
});
await check('trustDirectoryResolver tries each base', async () => {
const resolver = trustDirectoryResolver({ baseUrls: ['http://127.0.0.1:1', base] });
const resolved = await resolver.resolve('abc123');
assert(resolved, 'expected fallback to second base');
assert(resolved.publicKeyPem.includes('BEGIN PUBLIC KEY'));
});
await check('resolveKey returns null when no resolver matches', async () => {
const resolved = await resolveKey('unknown:keyid', [
didWebResolver(),
directUrlResolver(),
]);
assertEq(resolved, null);
});
// ---- Endorsement tests ----
await check('verifyEndorsement round-trip with direct-URL resolver', async () => {
const { publicKey: ePub, privateKey: ePriv } = generateKeyPairSync('ed25519');
const ePem = ePub.export({ type: 'spki', format: 'pem' });
const endorsement = {
endorser: `${base}/key.json`,
endorsement: 'sha256:abcdef',
timestamp: '2026-04-28T12:00:00Z',
algorithm: 'ed25519',
};
const binding = buildEndorsementBinding(endorsement);
endorsement.signature = nodeSign(null, Buffer.from(binding), ePriv).toString('base64');
// Replace fixture key for the duration of this test by swapping the route
// via a fresh server (avoids mutating shared state).
const localFixture = await startFixtureServer({
'/key.json': () => ({ body: { publicKey: ePem, algorithm: 'ed25519' } }),
});
const localPort = localFixture.address().port;
endorsement.endorser = `http://127.0.0.1:${localPort}/key.json`;
// Re-sign with the corrected endorser keyid (binding doesn't include keyid
// so the existing signature still verifies).
const ok = await verifyEndorsement(endorsement, [directUrlResolver()]);
await new Promise((r) => localFixture.close(r));
assert(ok, 'expected endorsement to verify');
});
await check('verifyEndorsement fails on tampered hash', async () => {
const { publicKey: ePub, privateKey: ePriv } = generateKeyPairSync('ed25519');
const ePem = ePub.export({ type: 'spki', format: 'pem' });
const localFixture = await startFixtureServer({
'/key.json': () => ({ body: { publicKey: ePem, algorithm: 'ed25519' } }),
});
const localPort = localFixture.address().port;
const endorsement = {
endorser: `http://127.0.0.1:${localPort}/key.json`,
endorsement: 'sha256:original',
timestamp: '2026-04-28T12:00:00Z',
algorithm: 'ed25519',
};
endorsement.signature = nodeSign(
null,
Buffer.from(buildEndorsementBinding(endorsement)),
ePriv,
).toString('base64');
endorsement.endorsement = 'sha256:tampered';
const ok = await verifyEndorsement(endorsement, [directUrlResolver()]);
await new Promise((r) => localFixture.close(r));
assert(!ok, 'tampered endorsement must not verify');
});
await new Promise((r) => fixtureServer.close(r));
console.log(`\n${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);