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+
22+ const ec = new EC ( 'p256' ) ;
23+
1124export default class VerifyBundle extends Command {
1225 static override flags = {
1326 bundle : Flags . string ( {
@@ -34,55 +47,142 @@ export default class VerifyBundle extends Command {
3447 } ;
3548
3649 static override args = {
37- file : Args . file ( {
38- description : 'artifact to verify' ,
50+ fileOrDigest : Args . string ( {
51+ description : 'path to the artifact to verify, or its digest ' ,
3952 required : true ,
40- exists : true ,
4153 } ) ,
4254 } ;
4355
4456 public async run ( ) : Promise < void > {
4557 const { args, flags } = await this . parse ( VerifyBundle ) ;
4658
59+ const trustedRootPath = flags [ 'trusted-root' ] ;
4760 const bundle = await fs
4861 . readFile ( flags . bundle )
49- . then ( ( data ) => JSON . parse ( data . toString ( ) ) ) ;
50- const artifact = await fs . readFile ( args . file ) ;
51- const trustedRootPath = flags [ 'trusted-root' ] ;
62+ . then ( ( data ) => JSON . parse ( data . toString ( ) ) )
63+ . then ( ( json ) => bundleFromJSON ( json ) ) ;
64+
65+ const trustMaterial = trustedRootPath
66+ ? await trustMaterialFromPath ( trustedRootPath )
67+ : await trustMaterialFromTUF ( flags [ 'staging' ] ) ;
68+ const verifier = new Verifier ( trustMaterial ) ;
69+
70+ const policy = {
71+ subjectAlternativeName : flags [ 'certificate-identity' ] ,
72+ extensions : { issuer : flags [ 'certificate-oidc-issuer' ] } ,
73+ } ;
74+
75+ const signedEntity = isDigest ( args . fileOrDigest )
76+ ? await signedEntityFromDigest ( bundle , args . fileOrDigest )
77+ : await signedEntityFromFile ( bundle , args . fileOrDigest ) ;
78+
79+ verifier . verify ( signedEntity , policy ) ;
80+ }
81+ }
82+
83+ // Initialize TrustMaterial from TUF
84+ async function trustMaterialFromTUF ( staging : boolean ) : Promise < TrustMaterial > {
85+ const opts : tuf . TUFOptions = { } ;
86+
87+ if ( staging ) {
88+ // Write the initial root.json to a temporary directory
89+ const tmpPath = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'sigstore-' ) ) ;
90+ const rootPath = path . join ( tmpPath , 'root.json' ) ;
91+ await fs . writeFile ( rootPath , Buffer . from ( TUF_STAGING_ROOT , 'base64' ) ) ;
92+
93+ opts . mirrorURL = TUF_STAGING_URL ;
94+ opts . rootPath = rootPath ;
95+ }
96+
97+ const trustedRoot = await tuf . getTrustedRoot ( opts ) ;
98+ return toTrustMaterial ( trustedRoot ) ;
99+ }
100+
101+ // Initialize TrustMaterial from a file
102+ async function trustMaterialFromPath ( path : string ) : Promise < TrustMaterial > {
103+ const trustedRoot = await fs
104+ . readFile ( path )
105+ . then ( ( data ) => JSON . parse ( data . toString ( ) ) ) ;
106+ return toTrustMaterial ( TrustedRoot . fromJSON ( trustedRoot ) ) ;
107+ }
108+
109+ // Initialize SignedEntity with the artifact to verify
110+ async function signedEntityFromFile (
111+ bundle : Bundle ,
112+ fileOrDigest : string
113+ ) : Promise < SignedEntity > {
114+ const artifact = await fs . readFile ( fileOrDigest ) ;
115+ return toSignedEntity ( bundle , artifact ) ;
116+ }
117+
118+ // Initialize SignedEntity with the digest of the artifact to verify
119+ async function signedEntityFromDigest (
120+ bundle : Bundle ,
121+ digest : string
122+ ) : Promise < SignedEntity > {
123+ const signedEntity = toSignedEntity ( bundle ) ;
124+
125+ if ( bundle . content . $case === 'messageSignature' ) {
126+ signedEntity . signature = new MessageDigestSignatureContent (
127+ bundle . content . messageSignature ,
128+ digest . split ( ':' ) [ 1 ]
129+ ) ;
130+ }
131+
132+ return signedEntity ;
133+ }
134+
135+ function isDigest ( fileOrDigest : string ) : boolean {
136+ return (
137+ fileOrDigest . startsWith ( DIGEST_PREFIX ) &&
138+ fileOrDigest . length === 64 + DIGEST_PREFIX . length &&
139+ ! existsSync ( fileOrDigest )
140+ ) ;
141+ }
142+
143+ // Signature content implementation which can verify an artifact's signature
144+ // given the digest of the artifact. The default implementation requires the
145+ // artifact itself, which is not available when verifying a digest.
146+ // The crypto library in Node.js does not provide a way to verify a signature
147+ // given only the digest of the signed data. To work around this, we're using
148+ // the elliptic library to verify the signature on the digest directly.
149+ class MessageDigestSignatureContent {
150+ constructor (
151+ private messageSignature : MessageSignature ,
152+ private digest : string
153+ ) {
154+ this . messageSignature = messageSignature ;
155+ this . digest = digest ;
156+ }
157+
158+ get signature ( ) : Buffer {
159+ return this . messageSignature . signature ;
160+ }
161+
162+ public compareSignature ( signature : Buffer ) : boolean {
163+ return crypto . timingSafeEqual ( signature , this . signature ) ;
164+ }
165+
166+ public compareDigest ( digest : Buffer ) : boolean {
167+ return crypto . timingSafeEqual (
168+ digest ,
169+ this . messageSignature . messageDigest . digest
170+ ) ;
171+ }
172+
173+ verifySignature ( key : crypto . KeyObject ) : boolean {
174+ // Export public key to JWK format
175+ const jwk = key . export ( { format : 'jwk' } ) ;
176+
177+ // Create an elliptic-compatible key from the JWK
178+ const eckey = ec . keyFromPublic (
179+ {
180+ x : Buffer . from ( jwk . x ! , 'base64' ) . toString ( 'hex' ) ,
181+ y : Buffer . from ( jwk . y ! , 'base64' ) . toString ( 'hex' ) ,
182+ } ,
183+ 'hex'
184+ ) ;
52185
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- }
186+ return eckey . verify ( this . digest , this . signature ) ;
87187 }
88188}
0 commit comments