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,30 +37,101 @@ 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
+ 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} : {
36
109
signatures : Array < { keyid : string , sig : string } > ;
37
110
integrity : string ;
38
111
packageName : string ;
39
112
version : string ;
40
113
} ) {
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 ( ) ;
44
116
117
+ const keys = await verificationKeysCache ;
45
118
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 ) ;
47
124
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` ) ;
49
129
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
- ) ;
57
130
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
+ ) ;
59
135
}
60
136
}
61
137
@@ -65,7 +141,7 @@ export async function fetchLatestStableVersion(packageName: string) {
65
141
const { version, dist : { integrity, signatures, shasum} } = metadata ;
66
142
67
143
if ( ! shouldSkipIntegrityCheck ( ) ) {
68
- verifySignature ( {
144
+ await verifySignature ( {
69
145
packageName, version,
70
146
integrity, signatures,
71
147
} ) ;
0 commit comments