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,38 +37,112 @@ 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+ // base64 encoded DER SPKI
43+ keyData : string ;
44+ }
45+
46+ async function fetchSigstoreTufKeys ( ) : Promise < Array < KeyInfo > | null > {
47+ // This follows the implementation for npm.
48+ // See https://github.com/npm/cli/blob/3a80a7b7d168c23b5e297cba7b47ba5b9875934d/lib/utils/verify-signatures.js#L174
49+ let keysRaw : string ;
50+ try {
51+ // @ts -expect-error inject custom fetch into monkey-patched `tuf-js` module.
52+ globalThis . tufJsFetch = async ( input : string ) => {
53+ const agent = await httpUtils . getProxyAgent ( input ) ;
54+ return await globalThis . fetch ( input , {
55+ dispatcher : agent ,
56+ } ) ;
57+ } ;
58+ const sigstoreTufClient = await sigstoreTuf . initTUF ( {
59+ cachePath : path . join ( folderUtils . getCorepackHomeFolder ( ) , `_tuf` ) ,
60+ } ) ;
61+ keysRaw = await sigstoreTufClient . getTarget ( `registry.npmjs.org/keys.json` ) ;
62+ } catch ( error ) {
63+ console . warn ( `Warning: Failed to get signing keys from Sigstore TUF repo` , error ) ;
64+ return null ;
65+ }
66+
67+ // The format of the key file is undocumented but follows `PublicKey` from
68+ // sigstore/protobuf-specs.
69+ // See https://github.com/sigstore/protobuf-specs/blob/main/gen/pb-typescript/src/__generated__/sigstore_common.ts
70+ const keysFromSigstore = JSON . parse ( keysRaw ) as { keys : Array < { keyId : string , publicKey : { rawBytes : string , keyDetails : string } } > } ;
71+
72+ return keysFromSigstore . keys . filter ( key => {
73+ if ( key . publicKey . keyDetails === `PKIX_ECDSA_P256_SHA_256` ) {
74+ return true ;
75+ } else {
76+ debugUtils . log ( `Unsupported verification key type ${ key . publicKey . keyDetails } ` ) ;
77+ return false ;
78+ }
79+ } ) . map ( k => ( {
80+ keyid : k . keyId ,
81+ keyData : k . publicKey . rawBytes ,
82+ } ) ) ;
83+ }
84+
85+ async function getVerificationKeys ( ) : Promise < Array < KeyInfo > > {
86+ let keys : Array < { keyid : string , key : string } > ;
87+
88+ if ( process . env . COREPACK_INTEGRITY_KEYS ) {
89+ // We use the format of the `GET /-/npm/v1/keys` endpoint with `npm` instead
90+ // of `keys` as the wrapping key.
91+ const keysFromEnv = JSON . parse ( process . env . COREPACK_INTEGRITY_KEYS ) as { npm : Array < { keyid : string , key : string } > } ;
92+ keys = keysFromEnv . npm ;
93+ debugUtils . log ( `Using COREPACK_INTEGRITY_KEYS to verify signatures: ${ keys . map ( k => k . keyid ) . join ( `, ` ) } ` ) ;
94+ return keys . map ( k => ( {
95+ keyid : k . keyid ,
96+ keyData : k . key ,
97+ } ) ) ;
98+ }
99+
100+
101+ const sigstoreKeys = await fetchSigstoreTufKeys ( ) ;
102+ if ( sigstoreKeys ) {
103+ debugUtils . log ( `Using NPM keys from @sigstore/tuf to verify signatures: ${ sigstoreKeys . map ( k => k . keyid ) . join ( `, ` ) } ` ) ;
104+ return sigstoreKeys ;
105+ }
106+
107+ debugUtils . log ( `Falling back to built-in npm verification keys` ) ;
108+ return defaultConfig . keys . npm . map ( k => ( {
109+ keyid : k . keyid ,
110+ keyData : k . key ,
111+ } ) ) ;
112+ }
113+
114+ let verificationKeysCache : Promise < Array < KeyInfo > > | null = null ;
115+
116+ export async function verifySignature ( { signatures, integrity, packageName, version} : {
36117 signatures : Array < { keyid : string , sig : string } > ;
37118 integrity : string ;
38119 packageName : string ;
39120 version : string ;
40121} ) {
41122 if ( ! Array . isArray ( signatures ) || ! signatures . length ) throw new Error ( `No compatible signature found in package metadata` ) ;
42123
43- const { npm : trustedKeys } = process . env . COREPACK_INTEGRITY_KEYS ?
44- JSON . parse ( process . env . COREPACK_INTEGRITY_KEYS ) as typeof defaultConfig . keys :
45- defaultConfig . keys ;
46-
47- let signature : typeof signatures [ 0 ] | undefined ;
48- let key ! : string ;
49- for ( const k of trustedKeys ) {
50- signature = signatures . find ( ( { keyid} ) => keyid === k . keyid ) ;
51- if ( signature != null ) {
52- key = k . key ;
53- break ;
54- }
55- }
56- if ( signature ?. sig == null ) throw new UsageError ( `The package was not signed by any trusted keys: ${ JSON . stringify ( { signatures, trustedKeys} , undefined , 2 ) } ` ) ;
57-
58- const verifier = createVerify ( `SHA256` ) ;
59- verifier . end ( `${ packageName } @${ version } :${ integrity } ` ) ;
60- const valid = verifier . verify (
61- `-----BEGIN PUBLIC KEY-----\n${ key } \n-----END PUBLIC KEY-----` ,
62- signature . sig ,
63- `base64` ,
64- ) ;
124+ if ( ! verificationKeysCache )
125+ verificationKeysCache = getVerificationKeys ( ) ;
126+
127+ const keys = await verificationKeysCache ;
128+ const keyInfo = keys . find ( ( { keyid} ) => signatures . some ( s => s . keyid === keyid ) ) ;
129+ if ( keyInfo == null )
130+ throw new Error ( `Cannot find key to verify signature. signature keys: ${ signatures . map ( s => s . keyid ) } , verification keys: ${ keys . map ( k => k . keyid ) } ` ) ;
131+
132+ const signature = signatures . find ( ( { keyid} ) => keyid === keyInfo . keyid ) ;
133+ assert ( signature ) ;
134+
135+ const verifier = crypto . createVerify ( `SHA256` ) ;
136+ const payload = `${ packageName } @${ version } :${ integrity } ` ;
137+ verifier . end ( payload ) ;
138+ const key = crypto . createPublicKey ( { key : Buffer . from ( keyInfo . keyData , `base64` ) , format : `der` , type : `spki` } ) ;
139+ const valid = verifier . verify ( key , signature . sig , `base64` ) ;
140+
65141 if ( ! valid ) {
66- throw new Error ( `Signature does not match` ) ;
142+ throw new Error (
143+ `Signature verification failed for ${ payload } with key ${ keyInfo . keyid } \n` +
144+ `If you are using a custom registry you can set COREPACK_INTEGRITY_KEYS.` ,
145+ ) ;
67146 }
68147}
69148
@@ -74,7 +153,7 @@ export async function fetchLatestStableVersion(packageName: string) {
74153
75154 if ( ! shouldSkipIntegrityCheck ( ) ) {
76155 try {
77- verifySignature ( {
156+ await verifySignature ( {
78157 packageName, version,
79158 integrity, signatures,
80159 } ) ;
0 commit comments