1
+ import * as sigstoreTuf from '@sigstore/tuf' ;
1
2
import { 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' ;
3
6
4
7
import defaultConfig from '../config.json' ;
5
8
6
9
import { shouldSkipIntegrityCheck } from './corepackUtils' ;
10
+ import * as debugUtils from './debugUtils' ;
11
+ import * as folderUtils from './folderUtils' ;
7
12
import * as httpUtils from './httpUtils' ;
8
13
9
14
// load abbreviated metadata as that's all we need for these calls
@@ -32,38 +37,112 @@ export async function fetchAsJson(packageName: string, version?: string) {
32
37
return httpUtils . fetchAsJson ( `${ npmRegistryUrl } /${ packageName } ${ version ? `/${ version } ` : `` } ` , { headers} ) ;
33
38
}
34
39
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} : {
36
117
signatures : Array < { keyid : string , sig : string } > ;
37
118
integrity : string ;
38
119
packageName : string ;
39
120
version : string ;
40
121
} ) {
41
122
if ( ! Array . isArray ( signatures ) || ! signatures . length ) throw new Error ( `No compatible signature found in package metadata` ) ;
42
123
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
+
65
141
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
+ ) ;
67
146
}
68
147
}
69
148
@@ -74,7 +153,7 @@ export async function fetchLatestStableVersion(packageName: string) {
74
153
75
154
if ( ! shouldSkipIntegrityCheck ( ) ) {
76
155
try {
77
- verifySignature ( {
156
+ await verifySignature ( {
78
157
packageName, version,
79
158
integrity, signatures,
80
159
} ) ;
0 commit comments