11import { Args , Command , Flags } from '@oclif/core' ;
2- import { bundleFromJSON } from '@sigstore/bundle' ;
2+ import { Bundle , bundleFromJSON , MessageSignature } from '@sigstore/bundle' ;
33import { TrustedRoot } from '@sigstore/protobuf-specs' ;
4- import { Verifier , toSignedEntity , toTrustMaterial } from '@sigstore/verify' ;
4+ import * as tuf from '@sigstore/tuf' ;
5+ import {
6+ SignedEntity ,
7+ toSignedEntity ,
8+ toTrustMaterial ,
9+ TrustMaterial ,
10+ Verifier ,
11+ } from '@sigstore/verify' ;
12+ import { ec as EC } from 'elliptic' ;
13+ import { existsSync } from 'fs' ;
514import fs from 'fs/promises' ;
15+ import crypto from 'node:crypto' ;
616import os from 'os' ;
717import path from 'path' ;
8- import * as sigstore from 'sigstore' ;
918import { TUF_STAGING_ROOT , TUF_STAGING_URL } from '../staging' ;
1019
20+ const DIGEST_PREFIX = 'sha256:' ;
21+
1122export default class VerifyBundle extends Command {
1223 static override flags = {
1324 bundle : Flags . string ( {
@@ -34,55 +45,155 @@ export default class VerifyBundle extends Command {
3445 } ;
3546
3647 static override args = {
37- file : Args . file ( {
38- description : 'artifact to verify' ,
48+ fileOrDigest : Args . string ( {
49+ description : 'path to the artifact to verify, or its digest ' ,
3950 required : true ,
40- exists : true ,
4151 } ) ,
4252 } ;
4353
4454 public async run ( ) : Promise < void > {
4555 const { args, flags } = await this . parse ( VerifyBundle ) ;
4656
57+ const trustedRootPath = flags [ 'trusted-root' ] ;
4758 const bundle = await fs
4859 . readFile ( flags . bundle )
49- . then ( ( data ) => JSON . parse ( data . toString ( ) ) ) ;
50- const artifact = await fs . readFile ( args . file ) ;
51- const trustedRootPath = flags [ 'trusted-root' ] ;
60+ . then ( ( data ) => JSON . parse ( data . toString ( ) ) )
61+ . then ( ( json ) => bundleFromJSON ( json ) ) ;
62+
63+ const trustMaterial = trustedRootPath
64+ ? await trustMaterialFromPath ( trustedRootPath )
65+ : await trustMaterialFromTUF ( flags [ 'staging' ] ) ;
66+ const verifier = new Verifier ( trustMaterial ) ;
67+
68+ const policy = {
69+ subjectAlternativeName : flags [ 'certificate-identity' ] ,
70+ extensions : { issuer : flags [ 'certificate-oidc-issuer' ] } ,
71+ } ;
72+
73+ const signedEntity = isDigest ( args . fileOrDigest )
74+ ? await signedEntityFromDigest ( bundle , args . fileOrDigest )
75+ : await signedEntityFromFile ( bundle , args . fileOrDigest ) ;
76+
77+ verifier . verify ( signedEntity , policy ) ;
78+ }
79+ }
80+
81+ // Initialize TrustMaterial from TUF
82+ async function trustMaterialFromTUF ( staging : boolean ) : Promise < TrustMaterial > {
83+ const opts : tuf . TUFOptions = { } ;
84+
85+ if ( staging ) {
86+ // Write the initial root.json to a temporary directory
87+ const tmpPath = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'sigstore-' ) ) ;
88+ const rootPath = path . join ( tmpPath , 'root.json' ) ;
89+ await fs . writeFile ( rootPath , Buffer . from ( TUF_STAGING_ROOT , 'base64' ) ) ;
90+
91+ opts . mirrorURL = TUF_STAGING_URL ;
92+ opts . rootPath = rootPath ;
93+ }
94+
95+ const trustedRoot = await tuf . getTrustedRoot ( opts ) ;
96+ return toTrustMaterial ( trustedRoot ) ;
97+ }
98+
99+ // Initialize TrustMaterial from a file
100+ async function trustMaterialFromPath ( path : string ) : Promise < TrustMaterial > {
101+ const trustedRoot = await fs
102+ . readFile ( path )
103+ . then ( ( data ) => JSON . parse ( data . toString ( ) ) ) ;
104+ return toTrustMaterial ( TrustedRoot . fromJSON ( trustedRoot ) ) ;
105+ }
106+
107+ // Initialize SignedEntity with the artifact to verify
108+ async function signedEntityFromFile (
109+ bundle : Bundle ,
110+ fileOrDigest : string
111+ ) : Promise < SignedEntity > {
112+ const artifact = await fs . readFile ( fileOrDigest ) ;
113+ return toSignedEntity ( bundle , artifact ) ;
114+ }
115+
116+ // Initialize SignedEntity with the digest of the artifact to verify
117+ async function signedEntityFromDigest (
118+ bundle : Bundle ,
119+ digest : string
120+ ) : Promise < SignedEntity > {
121+ const signedEntity = toSignedEntity ( bundle ) ;
122+
123+ if ( bundle . content . $case === 'messageSignature' ) {
124+ signedEntity . signature = new MessageDigestSignatureContent (
125+ bundle . content . messageSignature ,
126+ digest . split ( ':' ) [ 1 ]
127+ ) ;
128+ }
129+
130+ return signedEntity ;
131+ }
132+
133+ function isDigest ( fileOrDigest : string ) : boolean {
134+ return (
135+ fileOrDigest . startsWith ( DIGEST_PREFIX ) &&
136+ fileOrDigest . length === 64 + DIGEST_PREFIX . length &&
137+ ! existsSync ( fileOrDigest )
138+ ) ;
139+ }
140+
141+ // Signature content implementation which can verify an artifact's signature
142+ // given the digest of the artifact. The default implementation requires the
143+ // artifact itself, which is not available when verifying a digest.
144+ // The crypto library in Node.js does not provide a way to verify a signature
145+ // given only the digest of the signed data. To work around this, we're using
146+ // the elliptic library to verify the signature on the digest directly.
147+ class MessageDigestSignatureContent {
148+ constructor (
149+ private messageSignature : MessageSignature ,
150+ private digest : string
151+ ) {
152+ this . messageSignature = messageSignature ;
153+ this . digest = digest ;
154+ }
155+
156+ get signature ( ) : Buffer {
157+ return this . messageSignature . signature ;
158+ }
159+
160+ public compareSignature ( signature : Buffer ) : boolean {
161+ return crypto . timingSafeEqual ( signature , this . signature ) ;
162+ }
163+
164+ public compareDigest ( digest : Buffer ) : boolean {
165+ return crypto . timingSafeEqual (
166+ digest ,
167+ this . messageSignature . messageDigest . digest
168+ ) ;
169+ }
170+
171+ verifySignature ( key : crypto . KeyObject ) : boolean {
172+ // Export public key to JWK format
173+ const jwk = key . export ( { format : 'jwk' } ) ;
174+ const ec = initEC ( key . asymmetricKeyDetails ?. namedCurve ) ;
175+
176+ // Create an elliptic-compatible key from the JWK
177+ const eckey = ec . keyFromPublic (
178+ {
179+ x : Buffer . from ( jwk . x ! , 'base64' ) . toString ( 'hex' ) ,
180+ y : Buffer . from ( jwk . y ! , 'base64' ) . toString ( 'hex' ) ,
181+ } ,
182+ 'hex'
183+ ) ;
184+
185+ return eckey . verify ( this . digest , this . signature ) ;
186+ }
187+ }
52188
53- if ( ! trustedRootPath ) {
54- const options : Parameters < typeof sigstore . verify > [ 2 ] = {
55- certificateIdentityURI : flags [ 'certificate-identity' ] ,
56- certificateIssuer : flags [ 'certificate-oidc-issuer' ] ,
57- } ;
58-
59- if ( flags [ 'staging' ] ) {
60- // Write the initial root.json to a temporary directory
61- const tmpPath = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'sigstore-' ) ) ;
62- const rootPath = path . join ( tmpPath , 'root.json' ) ;
63- await fs . writeFile ( rootPath , Buffer . from ( TUF_STAGING_ROOT , 'base64' ) ) ;
64-
65- options . tufMirrorURL = TUF_STAGING_URL ;
66- options . tufRootPath = rootPath ;
67- }
68-
69- sigstore . verify ( bundle , artifact , options ) ;
70- } else {
71- // Need to assemble the Verifier manually to pass in the trusted root
72- const trustedRoot = await fs
73- . readFile ( trustedRootPath )
74- . then ( ( data ) => JSON . parse ( data . toString ( ) ) ) ;
75- const trustMaterial = toTrustMaterial ( TrustedRoot . fromJSON ( trustedRoot ) ) ;
76- const signedEntity = toSignedEntity ( bundleFromJSON ( bundle ) , artifact ) ;
77- const policy = {
78- subjectAlternativeName : flags [ 'certificate-identity' ] ,
79- extensions : {
80- issuer : flags [ 'certificate-oidc-issuer' ] ,
81- } ,
82- } ;
83-
84- const verifier = new Verifier ( trustMaterial ) ;
85- verifier . verify ( signedEntity , policy ) ;
86- }
189+ function initEC ( curve : string | undefined ) : EC {
190+ console . log ( curve ) ;
191+ switch ( curve ) {
192+ case 'prime256v1' :
193+ return new EC ( 'p256' ) ;
194+ case 'secp384r1' :
195+ return new EC ( 'p384' ) ;
196+ default :
197+ throw new Error ( `unsupported curve: ${ curve } ` ) ;
87198 }
88199}
0 commit comments