import test from 'node:test'; import assert from 'node:assert/strict'; import {readFile} from 'node:fs/promises'; import {createHash, generateKeyPairSync} from 'node:crypto'; import {FallbackUnavailableError, KEY_SOURCE_LIMITS, makeKeyBundle, parsePublicKeys, resolveKeys, validateFallbackSources} from '../cf-worker/src/key-source.js'; import {SIGNED_BUNDLE_MAX_BYTES, makeSignedDocument, SIGNATURE_SEPARATOR} from '../cf-worker/src/signed-release.js'; const fixture = await readFile(new URL('./fixtures/keys.pub', import.meta.url), 'utf8'); const canonical = fixture.trim().split(/\s+/).slice(0, 2).join(' ') + '\n'; const revision = 'a'.repeat(40); const urls = ['https://keys.example.org/first.keys', 'https://keys.example.org/second.keys', 'https://keys.example.org/third.keys']; const emptyRelease = {keys: [], allowEmpty: false, revision, fallbackSources: urls}; const fixtureKeys = await parsePublicKeys(fixture); const hash = text => createHash('sha256').update(text).digest('hex'); const string = value => { const bytes = typeof value === 'string' ? Buffer.from(value) : Buffer.from(value); const prefix = Buffer.alloc(4); prefix.writeUInt32BE(bytes.length); return Buffer.concat([prefix, bytes]); }; const wire = (...fields) => Buffer.concat(fields.map(string)); const keyLine = (type, bytes, comment = '') => `${type} ${bytes.toString('base64')}${comment ? ` ${comment}` : ''}\n`; const secondFixture = keyLine('ssh-ed25519', wire('ssh-ed25519', Buffer.from('3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c', 'hex')), 'second'); const keyData = Buffer.from(fixture.split(/\s+/)[1], 'base64'); const response = text => new Response(text, {headers: {'Content-Type': 'text/plain; charset=utf-8'}}); async function unavailable(fetcher, options = {}, release = emptyRelease) { let caught; try { await resolveKeys(release, fetcher, options); } catch (error) { caught = error; } assert.ok(caught instanceof FallbackUnavailableError); assert.equal(caught.code, 'KEY_SOURCES_UNAVAILABLE'); return caught; } test('repository keys win without any upstream request and have complete protocol metadata', async () => { const result = await resolveKeys({...emptyRelease, keys: fixtureKeys}, () => assert.fail('Repository keys must not fetch')); assert.deepEqual(result.source, {type: 'repository'}); assert.equal(result.revision, revision); assert.equal(result.keyCount, 1); assert.equal(result.sha256, hash(canonical)); assert.equal(result.keyRevision, result.sha256); assert.equal(result.keysText, `# conf-sshd-v1\n# revision ${revision}\n# key-count 1\n# allow-empty no\n# sha256 ${hash(canonical)}\n# end-metadata\n${canonical}`); assert.ok(Number.isFinite(Date.parse(result.fetchedAt))); }); test('explicit empty revocation suppresses fallback; repository still wins when nonempty', async () => { const result = await resolveKeys({...emptyRelease, allowEmpty: true}, () => assert.fail('Revocation must not fetch')); assert.deepEqual(result.source, {type: 'explicit-empty'}); assert.equal(result.keyCount, 0); assert.equal(result.sha256, hash('')); assert.match(result.keysText, /# allow-empty yes\n/); const populated = await resolveKeys({...emptyRelease, keys: fixtureKeys, allowEmpty: true}, () => assert.fail('Must not fetch')); assert.equal(populated.source.type, 'repository'); assert.equal(populated.keyCount, 1); }); test('first valid nonempty source wins in order, without aggregation or redirect/cache reuse', async () => { const visited = []; const result = await resolveKeys(emptyRelease, async (url, init) => { visited.push(url); assert.equal(init.cache, 'no-store'); assert.equal(init.redirect, 'manual'); assert.equal(init.method, 'GET'); assert.match(init.headers['Cache-Control'], /no-store/); assert.ok(init.signal instanceof AbortSignal); return response(fixture); }); assert.deepEqual(visited, [urls[0]]); assert.deepEqual(result.source, {type: 'fallback', url: urls[0]}); assert.equal(result.keyCount, 1); assert.equal(result.sha256, hash(canonical)); }); test('malformed and empty sources are skipped before the next valid source', async () => { const visited = []; const result = await resolveKeys(emptyRelease, async url => { visited.push(url); return response([ 'upstream problem', '# no keys\n\n', fixture ][urls.indexOf(url)]); }); assert.deepEqual(visited, urls); assert.equal(result.source.url, urls[2]); }); for (const status of [301, 302, 307, 308, 400, 404, 429, 500, 503]) { test(`HTTP ${status} is skipped without following redirects`, async () => { const visited = []; const result = await resolveKeys(emptyRelease, async url => { visited.push(url); return url === urls[0] ? new Response('Do not return this body: SECRET', {status, headers: {Location: 'https://unconfigured.example/keys'}}) : response(fixture); }); assert.deepEqual(visited, urls.slice(0, 2)); assert.equal(result.source.url, urls[1]); }); } test('a response reported as redirected is rejected even when status is 200', async () => { const redirected = response(fixture); Object.defineProperty(redirected, 'redirected', {value: true}); const result = await resolveKeys(emptyRelease, async url => url === urls[0] ? redirected : response(secondFixture)); assert.equal(result.source.url, urls[1]); assert.equal(result.sha256, hash(secondFixture.trim().split(/\s+/).slice(0, 2).join(' ') + '\n')); }); test('fetch timeout is bounded even when an upstream ignores AbortSignal', async () => { const visited = []; const result = await resolveKeys(emptyRelease, async url => { visited.push(url); if (url === urls[0]) return new Promise(() => {}); return response(fixture); }, {timeoutMs: 20}); assert.deepEqual(visited, urls.slice(0, 2)); assert.equal(result.source.url, urls[1]); }); test('timeout includes streamed body and cancels it before falling back', async () => { let cancelled = false; const body = new ReadableStream({start(controller) {controller.enqueue(new TextEncoder().encode(fixture));}, cancel() {cancelled = true;}}); const result = await resolveKeys(emptyRelease, async url => url === urls[0] ? new Response(body) : response(secondFixture), {timeoutMs: 20}); assert.equal(cancelled, true); assert.equal(result.source.url, urls[1]); }); test('all failed sources return typed, bounded diagnostics without secret bodies or errors', async () => { const error = await unavailable(async url => { if (url === urls[0]) throw new Error('Credential SECRET network detail'); if (url === urls[1]) return new Response('SECRET body', {status: 503}); return response('# no keys\n'); }); assert.deepEqual(error.diagnostics, [{index: 0, code: 'network-error'}, {index: 1, code: 'http-error', status: 503}, {index: 2, code: 'empty'}]); assert.doesNotMatch(JSON.stringify(error), /SECRET|https:/); }); test('no repository keys and no configured sources is unavailable without fetching', async () => { const error = await unavailable(() => assert.fail('No implicit GitHub or other source'), {}, {...emptyRelease, fallbackSources: []}); assert.deepEqual(error.diagnostics, []); }); test('streamed size is enforced even without or with a misleading Content-Length', async () => { for (const headers of [{}, {'Content-Length': '1'}]) { let cancelled = false; const body = new ReadableStream({start(controller) {controller.enqueue(new Uint8Array(70)); controller.enqueue(new Uint8Array(70));}, cancel() {cancelled = true;}}); const error = await unavailable(async () => new Response(body, {headers}), {maxBytes: 128}, {...emptyRelease, fallbackSources: [urls[0]]}); assert.equal(error.diagnostics[0].code, 'too-large'); assert.equal(cancelled, true); } }); test('oversized declared length is rejected without reading the body', async () => { const error = await unavailable(async () => new Response(fixture, {headers: {'Content-Length': String(SIGNED_BUNDLE_MAX_BYTES + 1)}}), {}, {...emptyRelease, fallbackSources: [urls[0]]}); assert.equal(error.diagnostics[0].code, 'too-large'); }); test('invalid UTF-8 is rejected instead of silently replaced', async () => { const error = await unavailable(async () => new Response(Uint8Array.from([0xc3, 0x28])), {}, {...emptyRelease, fallbackSources: [urls[0]]}); assert.equal(error.diagnostics[0].code, 'invalid-utf8'); }); test('changing upstream changes content revision while build revision stays fixed; no stale fallback', async () => { let body = fixture; const fetcher = async () => response(body); const release = {...emptyRelease, fallbackSources: [urls[0]]}; const first = await resolveKeys(release, fetcher); body = secondFixture; const second = await resolveKeys(release, fetcher); assert.notEqual(first.keyRevision, second.keyRevision); assert.equal(first.revision, second.revision); body = ''; await assert.rejects(resolveKeys(release, fetcher), FallbackUnavailableError); }); test('octet-stream responses are accepted only after strict key parsing', async () => { const result = await resolveKeys(emptyRelease, async () => new Response(fixture, {headers: {'Content-Type': 'application/octet-stream'}})); assert.equal(result.keyCount, 1); }); test('sources are trusted HTTPS config only, bounded, without credentials or fragments', () => { assert.deepEqual(validateFallbackSources(urls), urls); assert.deepEqual(validateFallbackSources(), []); for (const invalid of [null, {}, new Array(6).fill(urls[0]), ['http://example.org/key'], ['https://user:password@example.org/key'], ['https://example.org/key#fragment'], ['https://example.org/key#'], ['https://example.org/key\n'], ['data:text/plain,key'], ['//example.org/key']]) assert.throws(() => validateFallbackSources(invalid)); }); test('Unicode comments and CRLF are canonicalized, with a known SHA256 fingerprint', async () => { const input = fixture.trim().split(/\s+/).slice(0, 2).join('\t') + '\t 家用服务器 🔐\t张三\r\n'; const [key] = await parsePublicKeys(input); assert.equal(key.label, '家用服务器 🔐 张三'); assert.equal(key.fingerprint, 'SHA256:bbXpuKG6zhzdmnxq256TlqzFBzRl2f6OOg722cYNbU8'); assert.equal(key.text, `${fixture.trim().split(/\s+/).slice(0, 2).join(' ')} 家用服务器 🔐 张三`); const bundle = await makeKeyBundle([key], revision, false); assert.equal(bundle.sha256, hash(canonical)); }); test('duplicates, options, certificates, private keys, invalid controls, and junk are rejected', async () => { const cert = fixture.replaceAll('ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com'); for (const invalid of [fixture + fixture, `restrict ${fixture}`, `command="id" ${fixture}`, cert, '-----BEGIN OPENSSH PRIVATE KEY-----\n', fixture + 'oops', fixture.replace('TEST-ONLY', 'bad\u0000comment'), fixture.replace('TEST-ONLY', 'bad\rcomment'), fixture.replace('TEST-ONLY', 'bad\u0085comment'), fixture.replace('TEST-ONLY', 'bad\ud800comment')]) { await assert.rejects(parsePublicKeys(invalid)); } }); test('wrong wire type, truncated fields, trailing data, noncanonical base64, invalid key sizes fail', async () => { const wrongType = keyLine('ssh-rsa', keyData); const truncated = keyLine('ssh-ed25519', keyData.subarray(0, keyData.length - 1)); const trailing = keyLine('ssh-ed25519', Buffer.concat([keyData, Buffer.from([0])])); const short = keyLine('ssh-ed25519', wire('ssh-ed25519', Buffer.alloc(31))); const long = keyLine('ssh-ed25519', wire('ssh-ed25519', Buffer.alloc(33))); const badBase64 = 'ssh-ed25519 A===\n'; const hugeField = keyLine('ssh-ed25519', Buffer.from([255, 255, 255, 255])); for (const input of [wrongType, truncated, trailing, short, long, badBase64, hugeField]) await assert.rejects(parsePublicKeys(input)); }); test('public key count and line/input size limits are enforced', async () => { await assert.rejects(parsePublicKeys(fixture + secondFixture, {maxKeys: 1}), /Too many/); await assert.rejects(parsePublicKeys(fixture.trim() + 'x'.repeat(8192)), /line exceeds/); await assert.rejects(parsePublicKeys('#'.repeat(KEY_SOURCE_LIMITS.maxBytes + 1)), /size limit/); await assert.rejects(resolveKeys(emptyRelease, () => assert.fail(), {timeoutMs: KEY_SOURCE_LIMITS.timeoutMs + 1}), /limit/); }); function mpint(bytes) {return bytes[0] & 128 ? Buffer.concat([Buffer.from([0]), bytes]) : bytes;} function rsaLine(jwk, exponent, modulus) { return keyLine('ssh-rsa', wire('ssh-rsa', exponent || mpint(Buffer.from(jwk.e, 'base64url')), modulus || mpint(Buffer.from(jwk.n, 'base64url')))); } test('RSA requires complete positive canonical mpints and at least 2048 bits', async () => { const {publicKey} = generateKeyPairSync('rsa', {modulusLength: 2048}); const jwk = publicKey.export({format: 'jwk'}); assert.equal((await parsePublicKeys(rsaLine(jwk)))[0].type, 'ssh-rsa'); const modulus = mpint(Buffer.from(jwk.n, 'base64url')); for (const [e, n] of [[Buffer.from([0, 3]), modulus], [Buffer.from([128]), modulus], [Buffer.from([2]), modulus], [Buffer.from([1]), modulus], [Buffer.from([3]), Buffer.from([0])], [Buffer.from([3]), Buffer.from([127, 3])], [Buffer.from([3]), modulus.subarray(1)], [Buffer.from([3]), Buffer.concat([Buffer.from([0]), modulus])]]) await assert.rejects(parsePublicKeys(rsaLine(jwk, e, n))); }); for (const [name, curve] of [['nistp256', 'P-256'], ['nistp384', 'P-384'], ['nistp521', 'P-521']]) { test(`ECDSA ${name} requires a matching curve and a valid point`, async () => { const {publicKey} = generateKeyPairSync('ec', {namedCurve: curve}); const jwk = publicKey.export({format: 'jwk'}); const type = `ecdsa-sha2-${name}`; const point = Buffer.concat([Buffer.from([4]), Buffer.from(jwk.x, 'base64url'), Buffer.from(jwk.y, 'base64url')]); assert.equal((await parsePublicKeys(keyLine(type, wire(type, name, point))))[0].type, type); await assert.rejects(parsePublicKeys(keyLine(type, wire(type, 'wrong', point)))); await assert.rejects(parsePublicKeys(keyLine(type, wire(type, name, point.subarray(1))))); const offCurve = Buffer.alloc(point.length); offCurve[0] = 4; await assert.rejects(parsePublicKeys(keyLine(type, wire(type, name, offCurve))), /Invalid ECDSA point/); }); } test('OpenSSH security-key Ed25519 and ECDSA formats validate applications and trailing data', async () => { const edType = 'sk-ssh-ed25519@openssh.com'; const publicBytes = keyData.subarray(keyData.length - 32); const ed = keyLine(edType, wire(edType, publicBytes, 'ssh:example')); assert.equal((await parsePublicKeys(ed))[0].type, edType); await assert.rejects(parsePublicKeys(keyLine(edType, wire(edType, publicBytes, '')))); await assert.rejects(parsePublicKeys(keyLine(edType, wire(edType, publicBytes, 'ssh:\u0000')))); await assert.rejects(parsePublicKeys(keyLine(edType, wire(edType, publicBytes)))); const {publicKey} = generateKeyPairSync('ec', {namedCurve: 'P-256'}); const jwk = publicKey.export({format: 'jwk'}); const ecType = 'sk-ecdsa-sha2-nistp256@openssh.com'; const point = Buffer.concat([Buffer.from([4]), Buffer.from(jwk.x, 'base64url'), Buffer.from(jwk.y, 'base64url')]); assert.equal((await parsePublicKeys(keyLine(ecType, wire(ecType, 'nistp256', point, 'ssh:example'))))[0].type, ecType); }); test('bundle rejects implicit empty and invalid deployment metadata', async () => { await assert.rejects(makeKeyBundle([], revision, false)); await assert.rejects(makeKeyBundle(fixtureKeys, 'not-a-revision', false)); await assert.rejects(makeKeyBundle(fixtureKeys, revision, 'yes')); }); test('fallback URL configuration rejects queries, local/private names, and normalized duplicates', () => { for (const url of ['https://keys.example.org/key?token=secret', 'https://keys.example.org/key?', 'https://localhost/key', 'https://thing.local/key', 'https://metadata.google.internal/key', 'https://home.lan/key', 'https://192.168.1.1/key', 'https://127.1/key', 'https://[::1]/key', 'https://[2001:db8::1]/key', 'https://2130706433/key', 'https://0x7f000001/key', 'https://single-label/key']) assert.throws(() => validateFallbackSources([url])); assert.throws(() => validateFallbackSources(['https://KEYS.example.org:443/key', 'https://keys.example.org/key']), /Duplicate/); assert.deepEqual(validateFallbackSources(['https://KEYS.example.org:443/key']), ['https://keys.example.org/key']); }); test('public-key parser honors an aborted signal before and during validation', async () => { const controller = new AbortController(); controller.abort(); await assert.rejects(parsePublicKeys(fixture, {signal: controller.signal}), /timeout/); let reads = 0; const signal = {get aborted() {return ++reads >= 3;}}; await assert.rejects(parsePublicKeys(fixture + secondFixture, {signal}), /timeout/); }); test('whole serialized protocol, including metadata, is limited to the client response cap', async () => { const text = 'x'.repeat(KEY_SOURCE_LIMITS.maxBytes - 100); await assert.rejects(makeKeyBundle([{text}], revision, false), /too-large/); const bundle = await makeKeyBundle([{text: 'x'.repeat(KEY_SOURCE_LIMITS.maxBytes - 300)}], revision, false); assert.ok(Buffer.byteLength(bundle.text) <= KEY_SOURCE_LIMITS.maxBytes); }); test('large upstream comments are stripped before publication without leaking labels', async () => { const publicBytes = keyData.subarray(keyData.length - 32); const lines = Array.from({length: 128}, (_, index) => { const bytes = Buffer.from(publicBytes); bytes[31] ^= index; const prefix = keyLine('ssh-ed25519', wire('ssh-ed25519', bytes)).trim() + ' '; return prefix + 'x'.repeat(8190 - prefix.length) + '\n'; }).join(''); assert.ok(Buffer.byteLength(lines) <= KEY_SOURCE_LIMITS.maxBytes); const output = await resolveKeys({...emptyRelease, fallbackSources: [urls[0]]}, async () => response(lines)); assert.equal(output.keyCount, 128); assert.ok(output.keysText.length < 20000); assert.equal(output.keys[0].label, 'Server key 1'); const result = await resolveKeys(emptyRelease, async url => response(url === urls[0] ? lines : fixture)); assert.equal(result.source.url, urls[0]); assert.equal(result.keyCount, 128); }); test('ordinary private-key advice in comments is allowed, while real private formats are rejected anywhere', async () => { const advice = '# Do not copy private keys into this repository.\n'; assert.deepEqual(await parsePublicKeys(advice), []); const publicWithComment = fixture.trim().split(/\s+/).slice(0, 2).join(' ') + ' private key remains on my laptop\n'; assert.equal((await parsePublicKeys(advice + publicWithComment))[0].label, 'private key remains on my laptop'); for (const marker of ['-----BEGIN OPENSSH PRIVATE KEY-----', '-----BEGIN RSA PRIVATE KEY-----', '-----BEGIN EC PRIVATE KEY-----', '-----BEGIN PRIVATE KEY-----', '-----BEGIN ENCRYPTED PRIVATE KEY-----', 'PuTTY-User-Key-File-3: ssh-ed25519']) { await assert.rejects(parsePublicKeys('# ' + marker + '\n' + fixture), /Private keys are never accepted/); await assert.rejects(parsePublicKeys(publicWithComment.trim() + ' ' + marker + '\n'), /Private keys are never accepted/); } }); const gistSource = {type:'gist',id:'a'.repeat(32),ownerId:12345,ownerLogin:'original-login',file:'ssh.keys',signatureFile:'ssh.keys.sig',bundleFile:'release.bundle'}; const gistRevision = 'b'.repeat(40); const gistRelease = {...emptyRelease,keySource:gistSource}; const signedPolicy = {listId:'production',sequence:9,issuedAt:100,expiresAt:300}; const signedDocument = await makeSignedDocument(fixture,signedPolicy); // Syntactic fixture only, deliberately NOT a valid cryptographic signature. Worker never makes that claim. const signatureFixture = '-----BEGIN PGP SIGNATURE-----\n\nYWJj\n-----END PGP SIGNATURE-----\n'; const signedBundleFixture = signedDocument + SIGNATURE_SEPARATOR + signatureFixture; const gistFile = (filename,content) => ({filename,content,size:Buffer.byteLength(content),truncated:false,encoding:'utf-8',raw_url:'https://untrusted.invalid/ignored'}); const gistData = (files={}) => ({id:gistSource.id,owner:{id:gistSource.ownerId,login:'renamed-login'},public:true,truncated:false,history:[{version:gistRevision}],files}); const jsonResponse = data => response(JSON.stringify(data)); const gistUrl = `https://api.github.com/gists/${gistSource.id}`; for (const status of [201,204,206]) test(`non-200 success HTTP ${status} is not a key release`, async () => { const result=await resolveKeys(emptyRelease,async url=>url===urls[0]?new Response(status===204?null:fixture,{status}):response(fixture)); assert.equal(result.source.url,urls[1]); }); test('Git signed document is authoritative and exact, while missing/bad signature does not block OFF', async () => { for (const signature of [null,'bad armor',signatureFixture]) { const result=await resolveKeys({...emptyRelease,keys:fixtureKeys,signedDocument,signature},()=>assert.fail('No network')); assert.equal(result.keysText,signedDocument); assert.equal(result.signedDocument,signedDocument); assert.equal(result.signature,signature===signatureFixture?signatureFixture:null); assert.equal(result.keyRevision,hash(signedDocument)); assert.equal(result.sha256,hash(canonical)); assert.equal(result.signedMetadata.sequence,9); } }); test('signed empty revocation suppresses fallback even when legacy allowEmpty is false', async () => { const document=await makeSignedDocument('',{...signedPolicy,revoke:true}); const result=await resolveKeys({...emptyRelease,signedDocument:document,signature:signatureFixture},()=>assert.fail('Revocation must not fetch')); assert.equal(result.keyCount,0);assert.equal(result.allowEmpty,true);assert.equal(result.source.type,'explicit-empty'); assert.equal(result.keysText,document); }); test('strict sources never downgrade to raw keys; ordered signed fallback chooses one entire bundle', async () => { const visited=[]; const result=await resolveKeys({...emptyRelease,keys:fixtureKeys},async url=>{visited.push(url);return response(url===urls[0]?fixture:signedBundleFixture);},{requireSigned:true,now:200}); assert.deepEqual(visited,urls.slice(0,2));assert.equal(result.source.url,urls[1]);assert.equal(result.signature,signatureFixture); assert.equal(result.keysText,signedDocument);assert.equal(result.signedBundle,signedBundleFixture); }); test('fallback standalone signed document gets only its same-source detached signature', async () => { const visited=[]; const result=await resolveKeys(emptyRelease,async url=>{visited.push(url);return response(url.endsWith('.sig')?signatureFixture:signedDocument);},{requireSigned:true,now:200}); assert.deepEqual(visited,[urls[0],urls[0]+'.sig']);assert.equal(result.signature,signatureFixture);assert.equal(result.keysText,signedDocument); }); test('missing, non200, malformed or hung optional signature still serves the valid OFF document', async () => { for (const mode of ['missing','non200','bad','fetch-timeout','body-timeout']) { let cancelled=false; const body=new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('part'));},cancel(){cancelled=true;}}); const fetcher=async url=> { if (!url.endsWith('.sig')) return response(signedDocument); if(mode==='missing')return new Response('missing',{status:404}); if(mode==='non200')return new Response(signatureFixture,{status:206}); if(mode==='bad')return response('bad'); if(mode==='fetch-timeout')return new Promise(()=>{}); return new Response(body); }; const result=await resolveKeys(emptyRelease,fetcher,{timeoutMs:20}); assert.equal(result.keysText,signedDocument,mode);assert.equal(result.signature,null,mode); if(mode==='body-timeout')assert.equal(cancelled,true); } }); test('strict signature failure stays with its source before trying next whole signed release', async () => { const result=await resolveKeys(emptyRelease,async url=>url===urls[0]?response(signedDocument):url===urls[0]+'.sig'?new Response('',{status:404}):response(signedBundleFixture),{requireSigned:true,now:200}); assert.equal(result.source.url,urls[1]);assert.equal(result.signedBundle,signedBundleFixture); }); test('Gist fetch uses numeric-owner pin and immutable revision, never latest contents or raw_url', async () => { const visited=[]; const result=await resolveKeys(gistRelease,async (url,init)=>{ visited.push(url);assert.equal(init.headers['User-Agent'],'conf-sshd-key-source');assert.equal(init.redirect,'manual');assert.equal(init.cache,'no-store'); if(url===gistUrl)return jsonResponse(gistData({'ssh.keys':gistFile('ssh.keys',secondFixture)})); assert.equal(url,gistUrl+'/'+gistRevision); return jsonResponse(gistData({'ssh.keys':gistFile('ssh.keys',fixture)})); }); assert.deepEqual(visited,[gistUrl,gistUrl+'/'+gistRevision]); assert.equal(result.sha256,hash(canonical));assert.equal(result.source.type,'gist');assert.equal(result.source.ownerId,12345); assert.equal(result.source.ownerLogin,'original-login');assert.equal(result.source.gistRevision,gistRevision); assert.equal(result.signature,null); }); test('Gist signed document and signature are extracted from one pinned revision without transforms', async () => { let calls=0; const result=await resolveKeys(gistRelease,async url=>{calls++;return jsonResponse(gistData(url===gistUrl?{}:{'ssh.keys':gistFile('ssh.keys',signedDocument),'ssh.keys.sig':gistFile('ssh.keys.sig',signatureFixture)}));},{requireSigned:true,now:200}); assert.equal(calls,2);assert.equal(result.keysText,signedDocument);assert.equal(result.signature,signatureFixture);assert.equal(result.keyRevision,hash(signedDocument)); }); test('Gist bundle has one authoritative document and explicit revocation cannot be refilled', async () => { const revoke=await makeSignedDocument('',{...signedPolicy,revoke:true}); const bundle=revoke+SIGNATURE_SEPARATOR+signatureFixture; const result=await resolveKeys(gistRelease,async url=>{assert.ok(url.startsWith(gistUrl));return jsonResponse(gistData({'release.bundle':gistFile('release.bundle',bundle),'ssh.keys':gistFile('ssh.keys',fixture)}));}); assert.equal(result.keyCount,0);assert.equal(result.allowEmpty,true);assert.equal(result.keysText,revoke);assert.equal(result.source.file,'release.bundle'); }); test('missing/truncated/malformed Gist signature does not prevent an OFF document response', async () => { for(const optional of [null,gistFile('ssh.keys.sig','bad'),{...gistFile('ssh.keys.sig',signatureFixture),truncated:true}]) { const result=await resolveKeys(gistRelease,async()=>jsonResponse(gistData({'ssh.keys':gistFile('ssh.keys',signedDocument),...(optional?{'ssh.keys.sig':optional}:{})}))); assert.equal(result.keysText,signedDocument);assert.equal(result.signature,null); } }); for (const when of ['latest','revision']) test(`Gist owner mismatch in ${when} fails closed before ordered fallback`, async () => { const visited=[]; const result=await resolveKeys(gistRelease,async url=>{ visited.push(url); if(url===urls[0])return response(fixture); const data=gistData({'ssh.keys':gistFile('ssh.keys',fixture)}); if((when==='latest')===(url===gistUrl))data.owner.id=99999; return jsonResponse(data); }); assert.equal(result.source.type,'fallback');assert.equal(visited.length,when==='latest'?2:3); }); test('Gist rejects top-level truncation, private sources, invalid revision, selected-file truncation and byte mismatches', async () => { const mutations=[data=>{data.truncated=true;},data=>{data.public=false;},data=>{data.history[0].version='../latest';},data=>{data.files['ssh.keys'].truncated=true;},data=>{data.files['ssh.keys'].size++;},data=>{data.files['ssh.keys'].filename='different';},data=>{data.files['ssh.keys'].encoding='base64';},data=>{data.id='b'.repeat(32);}]; for(const mutate of mutations){ const result=await resolveKeys(gistRelease,async url=>{if(url===urls[0])return response(fixture);const data=gistData({'ssh.keys':gistFile('ssh.keys',fixture)});mutate(data);return jsonResponse(data);}); assert.equal(result.source.type,'fallback'); } }); test('Gist combined metadata/revision HTTP/body timeout is bounded and falls back without stale keys', async () => { for(const mode of ['http','json','fetch','body']) { let cancelled=false; const body=new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('{'));},cancel(){cancelled=true;}}); const result=await resolveKeys(gistRelease,async url=>{ if(url===urls[0])return response(fixture); if(url===gistUrl)return jsonResponse(gistData()); if(mode==='http')return new Response('',{status:503}); if(mode==='json')return response('{'); if(mode==='fetch')return new Promise(()=>{}); return new Response(body); },{timeoutMs:20}); assert.equal(result.source.type,'fallback');if(mode==='body')assert.equal(cancelled,true); } }); test('Gist dynamic revision changes content digest while code revision stays fixed', async () => { let current=gistRevision, text=fixture; const fetcher=async url=>{const data=gistData({'ssh.keys':gistFile('ssh.keys',text)});data.history[0].version=current;assert.ok(url===gistUrl||url===gistUrl+'/'+current);return jsonResponse(data);}; const first=await resolveKeys(gistRelease,fetcher); current='c'.repeat(40);text=secondFixture; const second=await resolveKeys(gistRelease,fetcher); assert.notEqual(first.sha256,second.sha256);assert.notEqual(first.source.gistRevision,second.source.gistRevision);assert.equal(first.revision,second.revision); }); test('invalid Gist and signing config is rejected before any network request', async () => { for(const keySource of [null,{type:'other'},{type:'git',url:'https://evil.example'}, {...gistSource,ownerId:'12345'}, {...gistSource,ownerId:0}, {...gistSource,id:'../escape'}, {...gistSource,ownerLogin:'bad/login'}, {...gistSource,file:'../key'}, {...gistSource,signatureFile:'ssh.keys'}, {...gistSource,token:'secret'}]) await assert.rejects(resolveKeys({...gistRelease,keySource},()=>assert.fail('Invalid config must not fetch'))); }); test('Gist response bytes are bounded and no fallback response inherits upstream text or private data', async () => { const result=await resolveKeys(gistRelease,async url=>url===urls[0]?response(fixture):new Response('{}',{headers:{'Content-Length':String(2*SIGNED_BUNDLE_MAX_BYTES+1)}})); assert.equal(result.source.type,'fallback');assert.doesNotMatch(JSON.stringify(result),/TEST-ONLY/); }); test('named Gist bundles cannot silently downgrade to raw key files', async () => { for(const contents of [fixture,signedDocument]) { const result=await resolveKeys(gistRelease,async url=>url===urls[0]?response(fixture):jsonResponse(gistData({'release.bundle':gistFile('release.bundle',contents)}))); assert.equal(result.source.type,'fallback'); } });