1+ import * as sigstoreTuf from '@sigstore/tuf' ;
12import { UsageError } from 'clipanion' ;
2- import { createVerify } from 'crypto' ;
3+ import assert from 'node:assert' ;
4+ import * as crypto from 'node:crypto' ;
5+ import * as path from 'node:path' ;
36
47import defaultConfig from '../config.json' ;
58
69import { shouldSkipIntegrityCheck } from './corepackUtils' ;
10+ import * as debugUtils from './debugUtils' ;
11+ import * as folderUtils from './folderUtils' ;
712import * as httpUtils from './httpUtils' ;
813
914// load abbreviated metadata as that's all we need for these calls
@@ -32,30 +37,101 @@ export async function fetchAsJson(packageName: string, version?: string) {
3237 return httpUtils . fetchAsJson ( `${ npmRegistryUrl } /${ packageName } ${ version ? `/${ version } ` : `` } ` , { headers} ) ;
3338}
3439
35- export function verifySignature ( { signatures, integrity, packageName, version} : {
40+ interface KeyInfo {
41+ keyid : string ;
42+ key : crypto . KeyObject ;
43+ }
44+
45+ async function fetchSigstoreTufKeys ( ) : Promise < Array < KeyInfo > | null > {
46+ // This follows the implementation for npm.
47+ // See https://github.com/npm/cli/blob/3a80a7b7d168c23b5e297cba7b47ba5b9875934d/lib/utils/verify-signatures.js#L174
48+ let keysRaw : string ;
49+ try {
50+ const sigstoreTufClient = await sigstoreTuf . initTUF ( { cachePath : path . join ( folderUtils . getCorepackHomeFolder ( ) , `_tuf` ) } ) ;
51+ keysRaw = await sigstoreTufClient . getTarget ( `registry.npmjs.org/keys.json` ) ;
52+ } catch ( error ) {
53+ console . warn ( `Failed to get signing keys from Sigstore TUF repo` , error ) ;
54+ return null ;
55+ }
56+
57+ // The format of the key file is undocumented but follows `PublicKey` from
58+ // sigstore/protobuf-specs.
59+ // See https://github.com/sigstore/protobuf-specs/blob/main/gen/pb-typescript/src/__generated__/sigstore_common.ts
60+ const keysFromSigstore = JSON . parse ( keysRaw ) as { keys : Array < { keyId : string , publicKey : { rawBytes : string , keyDetails : string } } > } ;
61+
62+ return keysFromSigstore . keys . filter ( key => {
63+ if ( key . publicKey . keyDetails === `PKIX_ECDSA_P256_SHA_256` ) {
64+ return true ;
65+ } else {
66+ debugUtils . log ( `Unsupported verification key type ${ key . publicKey . keyDetails } ` ) ;
67+ return false ;
68+ }
69+ } ) . map ( k => ( {
70+ keyid : k . keyId ,
71+ key : crypto . createPublicKey ( { key : Buffer . from ( k . publicKey . rawBytes , `base64` ) , format : `der` , type : `spki` } ) ,
72+ } ) ) ;
73+ }
74+
75+ async function getVerificationKeys ( ) : Promise < Array < KeyInfo > > {
76+ let keys : Array < { keyid : string , key : string } > ;
77+
78+ if ( process . env . COREPACK_INTEGRITY_KEYS ) {
79+ // We use the format of the `GET /-/npm/v1/keys` endpoint with `npm` instead
80+ // of `keys` as the wrapping key.
81+ const keysFromEnv = JSON . parse ( process . env . COREPACK_INTEGRITY_KEYS ) as { npm : Array < { keyid : string , key : string } > } ;
82+ keys = keysFromEnv . npm ;
83+ debugUtils . log ( `Using COREPACK_INTEGRITY_KEYS to verify signatures: ${ keys . map ( k => k . keyid ) . join ( `, ` ) } ` ) ;
84+ return keys . map ( k => ( {
85+ keyid : k . keyid ,
86+ key : crypto . createPublicKey ( `-----BEGIN PUBLIC KEY-----\n${ k . key } \n-----END PUBLIC KEY-----` ,
87+ ) ,
88+ } ) ) ;
89+ }
90+
91+
92+ const sigstoreKeys = await fetchSigstoreTufKeys ( ) ;
93+ if ( sigstoreKeys ) {
94+ debugUtils . log ( `Using NPM keys from @sigstore/tuf to verify signatures: ${ sigstoreKeys . map ( k => k . keyid ) . join ( `, ` ) } ` ) ;
95+ return sigstoreKeys ;
96+ }
97+
98+ debugUtils . log ( `Falling back to built-in npm verification keys` ) ;
99+ return defaultConfig . keys . npm . map ( k => ( {
100+ keyid : k . keyid ,
101+ key : crypto . createPublicKey ( `-----BEGIN PUBLIC KEY-----\n${ k . key } \n-----END PUBLIC KEY-----` ,
102+ ) ,
103+ } ) ) ;
104+ }
105+
106+ let verificationKeysCache : Promise < Array < { keyid : string , key : crypto . KeyObject } > > | null = null ;
107+
108+ export async function verifySignature ( { signatures, integrity, packageName, version} : {
36109 signatures : Array < { keyid : string , sig : string } > ;
37110 integrity : string ;
38111 packageName : string ;
39112 version : string ;
40113} ) {
41- const { npm : keys } = process . env . COREPACK_INTEGRITY_KEYS ?
42- JSON . parse ( process . env . COREPACK_INTEGRITY_KEYS ) as typeof defaultConfig . keys :
43- defaultConfig . keys ;
114+ if ( ! verificationKeysCache )
115+ verificationKeysCache = getVerificationKeys ( ) ;
44116
117+ const keys = await verificationKeysCache ;
45118 const key = keys . find ( ( { keyid} ) => signatures . some ( s => s . keyid === keyid ) ) ;
46- const signature = signatures . find ( ( { keyid} ) => keyid === key ?. keyid ) ;
119+ if ( key == null )
120+ throw new Error ( `Cannot find key to verify signature. signature keys: ${ signatures . map ( s => s . keyid ) } , verification keys: ${ keys . map ( k => k . keyid ) } ` ) ;
121+
122+ const signature = signatures . find ( ( { keyid} ) => keyid === key . keyid ) ;
123+ assert ( signature ) ;
47124
48- if ( key == null || signature == null ) throw new Error ( `Cannot find matching keyid: ${ JSON . stringify ( { signatures, keys} ) } ` ) ;
125+ const verifier = crypto . createVerify ( `SHA256` ) ;
126+ const payload = `${ packageName } @${ version } :${ integrity } ` ;
127+ verifier . end ( payload ) ;
128+ const valid = verifier . verify ( key , signature . sig , `base64` ) ;
49129
50- const verifier = createVerify ( `SHA256` ) ;
51- verifier . end ( `${ packageName } @${ version } :${ integrity } ` ) ;
52- const valid = verifier . verify (
53- `-----BEGIN PUBLIC KEY-----\n${ key . key } \n-----END PUBLIC KEY-----` ,
54- signature . sig ,
55- `base64` ,
56- ) ;
57130 if ( ! valid ) {
58- throw new Error ( `Signature does not match` ) ;
131+ throw new Error (
132+ `Signature verification failed for ${ payload } with key ${ key . keyid } \n` +
133+ `If you are using a custom registry you can set COREPACK_INTEGRITY_KEYS.` ,
134+ ) ;
59135 }
60136}
61137
@@ -65,7 +141,7 @@ export async function fetchLatestStableVersion(packageName: string) {
65141 const { version, dist : { integrity, signatures, shasum} } = metadata ;
66142
67143 if ( ! shouldSkipIntegrityCheck ( ) ) {
68- verifySignature ( {
144+ await verifySignature ( {
69145 packageName, version,
70146 integrity, signatures,
71147 } ) ;
0 commit comments