1- import { UsageError } from 'clipanion' ;
2- import { createVerify } from 'crypto' ;
1+ import * as sigstoreTuf from '@sigstore/tuf' ;
2+ import { UsageError } from 'clipanion' ;
3+ import assert from 'node:assert' ;
4+ import { createVerify , createPublicKey , KeyObject } from 'node:crypto' ;
35
4- import defaultConfig from '../config.json ' ;
5-
6- import { shouldSkipIntegrityCheck } from './corepackUtils ' ;
7- import * as httpUtils from './httpUtils ' ;
6+ import { shouldSkipIntegrityCheck } from './corepackUtils ' ;
7+ import * as debugUtils from './debugUtils' ;
8+ import * as httpUtils from './httpUtils ' ;
9+ import { once } from './utils ' ;
810
911// load abbreviated metadata as that's all we need for these calls
1012// see: https://github.com/npm/registry/blob/cfe04736f34db9274a780184d1cdb2fb3e4ead2a/docs/responses/package-metadata.md
@@ -32,30 +34,58 @@ export async function fetchAsJson(packageName: string, version?: string) {
3234 return httpUtils . fetchAsJson ( `${ npmRegistryUrl } /${ packageName } ${ version ? `/${ version } ` : `` } ` , { headers} ) ;
3335}
3436
35- export function verifySignature ( { signatures, integrity, packageName, version} : {
37+ const getVerificationKeys = once ( async ( ) : Promise < Array < { keyid : string , key : KeyObject } > > => {
38+ let keys : Array < { keyid : string , key : string } > ;
39+ if ( process . env . COREPACK_INTEGRITY_KEYS ) {
40+ // We use the format of the `GET /-/npm/v1/keys` endpoint with `npm` instead
41+ // of `keys` as the wrapping key.
42+ const keysFromEnv = JSON . parse ( process . env . COREPACK_INTEGRITY_KEYS ) as { npm : Array < { keyid : string , key : string } > } ;
43+ keys = keysFromEnv . npm . map ( k => ( { keyid : k . keyid , key : k . key } ) ) ;
44+ debugUtils . log ( `Using COREPACK_INTEGRITY_KEYS to verify signatures: ${ keys . map ( k => k . keyid ) . join ( `, ` ) } ` ) ;
45+ } else {
46+ // This follows the implementation for npm.
47+ // See https://github.com/npm/cli/blob/3a80a7b7d168c23b5e297cba7b47ba5b9875934d/lib/utils/verify-signatures.js#L174
48+ // We only support sigstore for NPM. For other registries
49+ // COREPACK_INTEGRITY_KEYS can be used.
50+ const sigstoreTufClient = await sigstoreTuf . initTUF ( ) ;
51+ const keysRaw = await sigstoreTufClient . getTarget ( `registry.npmjs.org/keys.json` ) ;
52+ // The format of the key file is undocumented, unfortunately. `rawBytes` is
53+ // the PEM content, i.e. base64 encoded SPKI DER.
54+ const keysFromSigstore = JSON . parse ( keysRaw ) as { keys : Array < { keyId : string , publicKey : { rawBytes : string } } > } ;
55+ keys = keysFromSigstore . keys . map ( k => ( { keyid : k . keyId , key : k . publicKey . rawBytes } ) ) ;
56+ debugUtils . log ( `Using NPM keys from @sigstore/tuf to verify signatures: ${ keys . map ( k => k . keyid ) . join ( `, ` ) } ` ) ;
57+ }
58+
59+ return keys . map ( k => ( {
60+ keyid : k . keyid ,
61+ key : createPublicKey ( `-----BEGIN PUBLIC KEY-----\n${ k . key } \n-----END PUBLIC KEY-----` ,
62+ ) } ) ) ;
63+ } ) ;
64+
65+ export async function verifySignature ( { signatures, integrity, packageName, version} : {
3666 signatures : Array < { keyid : string , sig : string } > ;
3767 integrity : string ;
3868 packageName : string ;
3969 version : string ;
4070} ) {
41- const { npm : keys } = process . env . COREPACK_INTEGRITY_KEYS ?
42- JSON . parse ( process . env . COREPACK_INTEGRITY_KEYS ) as typeof defaultConfig . keys :
43- defaultConfig . keys ;
44-
71+ const keys = await getVerificationKeys ( ) ;
4572 const key = keys . find ( ( { keyid} ) => signatures . some ( s => s . keyid === keyid ) ) ;
46- const signature = signatures . find ( ( { keyid} ) => keyid === key ?. keyid ) ;
73+ if ( key == null )
74+ throw new Error ( `Cannot find key to verify signature. signature keys: ${ signatures . map ( s => s . keyid ) } , verification keys: ${ keys . map ( k => k . keyid ) } ` ) ;
4775
48- if ( key == null || signature == null ) throw new Error ( `Cannot find matching keyid: ${ JSON . stringify ( { signatures, keys} ) } ` ) ;
76+ const signature = signatures . find ( ( { keyid} ) => keyid === key . keyid ) ;
77+ assert ( signature ) ; // If `key` is defined there is a matching signature
4978
5079 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- ) ;
80+ const payload = `${ packageName } @${ version } :${ integrity } ` ;
81+ verifier . end ( payload ) ;
82+ const valid = verifier . verify ( key , signature . sig , `base64` ) ;
83+
5784 if ( ! valid ) {
58- throw new Error ( `Signature does not match` ) ;
85+ throw new Error (
86+ `Signature verification failed for ${ payload } with key ${ key . keyid } \n` +
87+ `If you are using a custom registry you can set COREPACK_INTEGRITY_KEYS.` ,
88+ ) ;
5989 }
6090}
6191
@@ -65,7 +95,7 @@ export async function fetchLatestStableVersion(packageName: string) {
6595 const { version, dist : { integrity, signatures, shasum} } = metadata ;
6696
6797 if ( ! shouldSkipIntegrityCheck ( ) ) {
68- verifySignature ( {
98+ await verifySignature ( {
6999 packageName, version,
70100 integrity, signatures,
71101 } ) ;
0 commit comments