feat: pin client GPG trust and publish signed Git/Gist key releases

This commit is contained in:
2026-10-13 10:50:56 +08:00
parent 2f2f6db4d6
commit d72aa6eda7
28 changed files with 3187 additions and 427 deletions
+48 -3
View File
@@ -19,7 +19,7 @@ test('explicit endpoints ignore user agent and are never cached',async()=>{
}
});
test('root serves browser HTML and legacy curl script without content mixing',async()=>{
for(const agent of ['curl/8.1.0','libcurl/8.1.0']){
for(const agent of ['curl/8.1.0','libcurl/8.1.0','Wget/1.21.3','HTTPie/3.2.4']){
const response=await fetch(request('/',{headers:{'User-Agent':agent,Accept:'*/*'}}),env);
assert.equal(await response.text(),release.script);assert.equal(response.headers.get('Vary'),'Accept, User-Agent');
}
@@ -48,9 +48,9 @@ test('fallback page and key metadata represent one effective snapshot; unavailab
const fallback={...release,keys:[],keyCount:0,sha256:null,keysText:null,fallbackSources:['https://keys.example.com/server.keys']};
const handler=createHandler(fallback,{fetchImpl:async()=>new Response(fixture)});
const html=await (await handler(request('/'),env)).text();
assert.ok(html.includes('备用源可用'));assert.ok(html.includes('https://keys.example.com/server.keys'));assert.ok(html.includes(keys[0].fingerprint));
assert.ok(html.includes('备用源可用'));assert.ok(html.includes('https://keys.example.com/server.keys'));assert.ok(!html.includes(keys[0].fingerprint));assert.ok(!html.includes(keys[0].text));
const match=html.match(/<script id="release-data" type="application\/json">([\s\S]*?)<\/script>/);
const embedded=JSON.parse(match[1]);assert.equal(embedded.sha256,bundle.sha256);assert.equal(embedded.keyCount,1);assert.equal(embedded.keys[0].text,keys[0].text);
const embedded=JSON.parse(match[1]);assert.equal(embedded.sha256,bundle.sha256);assert.equal(embedded.keyCount,1);assert.equal(embedded.keys,undefined);
const metadata=await (await handler(request('/release.json'),env)).json();assert.equal(metadata.keyRevision,bundle.sha256);assert.equal(metadata.source.type,'fallback');
assert.equal(await (await handler(request('/ssh.keys'),env)).text(),bundle.text);
const unavailable=createHandler(fallback,{fetchImpl:async()=>new Response('',{status:503})});
@@ -64,3 +64,48 @@ test('explicit empty revocation suppresses fallback in page, metadata and keys',
const html=await (await handler(request('/'),env)).text();assert.ok(html.includes('明确撤销'));assert.ok(!html.includes('未发布撤销指令'));
const keysText=await (await handler(request('/ssh.keys'),env)).text();assert.match(keysText,/# key-count 0\n# allow-empty yes/);
});
test('signature endpoint never invents a signature for unsigned sources',async()=>{
for(const method of ['GET','HEAD']) {
const response=await fetch(request('/ssh.keys.sig',{method}),env);
assert.equal(response.status,404);
assert.equal(response.headers.get('Cache-Control'),'no-store');
if(method==='HEAD') assert.equal(await response.text(),'');
}
const metadata=await(await fetch(request('/release.json'),env)).json();
assert.equal(metadata.signatureAvailable,false);
assert.equal(metadata.signatureVerifiedByWorker,false);
assert.equal(metadata.signedMetadata,null);
});
test('public certificate is independent of unavailable key sources and never cached',async()=>{
assert.equal((await fetch(request('/public-key.gpg'),env)).status,404);
const certificate='-----BEGIN PGP PUBLIC KEY BLOCK-----\nfixture-public-only\n-----END PGP PUBLIC KEY BLOCK-----\n';
const handler=createHandler({...release,publicCertificate:certificate,keys:[],allowEmpty:false,fallbackSources:['https://bad.example.com/keys']},{fetchImpl:async()=>{throw Error('certificate must not resolve keys')}});
const response=await handler(request('/public-key.gpg'),env);
assert.equal(response.status,200);assert.equal(await response.text(),certificate);
assert.equal(response.headers.get('Cache-Control'),'no-store');
assert.equal(response.headers.get('Content-Type'),'application/pgp-keys');
assert.equal(await(await handler(request('/public-key.gpg',{method:'HEAD'}),env)).text(),'');
});
test('signature endpoint pairs with exact signed document, never the legacy wrapper',async()=>{
const {makeSignedDocument}=await import('../cf-worker/src/signed-release.js');
const document=await makeSignedDocument(fixture,{listId:'test-list',sequence:1,issuedAt:1700000000,expiresAt:1900000000});
// Structurally valid, deliberately non-cryptographic armor tests routing only.
const signature='-----BEGIN PGP SIGNATURE-----\n\nYWJj\n-----END PGP SIGNATURE-----\n';
const handler=createHandler({...release,signedDocument:document,signature});
const keysResponse=await handler(request('/ssh.keys'),env);
assert.equal(keysResponse.status,200);assert.equal(await keysResponse.text(),document);
const sigResponse=await handler(request('/ssh.keys.sig'),env);
assert.equal(sigResponse.status,200);assert.equal(await sigResponse.text(),signature);
assert.equal(sigResponse.headers.get('Content-Type'),'application/pgp-signature');
assert.equal(sigResponse.headers.get('Cache-Control'),'no-store');
const metadata=await(await handler(request('/release.json'),env)).json();
assert.equal(metadata.signatureAvailable,true);assert.equal(metadata.signatureVerifiedByWorker,false);
assert.equal(metadata.signedMetadata.sequence,1);
assert.equal(metadata.keyRevision,metadata.signedMetadata.documentSha256);
const unsigned=createHandler({...release,signedDocument:document,signature:null});
assert.equal(await(await unsigned(request('/ssh.keys'),env)).text(),document);
assert.equal((await unsigned(request('/ssh.keys.sig'),env)).status,404);
});