22
22
import dev .sigstore .VerificationOptions .CertificateMatcher ;
23
23
import dev .sigstore .VerificationOptions .UncheckedCertificateException ;
24
24
import dev .sigstore .bundle .Bundle ;
25
+ import dev .sigstore .bundle .Bundle .DsseEnvelope ;
25
26
import dev .sigstore .bundle .Bundle .MessageSignature ;
27
+ import dev .sigstore .dsse .InTotoPayload ;
26
28
import dev .sigstore .encryption .certificates .Certificates ;
27
29
import dev .sigstore .encryption .signers .Verifiers ;
28
30
import dev .sigstore .fulcio .client .FulcioVerificationException ;
33
35
import dev .sigstore .rekor .client .RekorTypes ;
34
36
import dev .sigstore .rekor .client .RekorVerificationException ;
35
37
import dev .sigstore .rekor .client .RekorVerifier ;
38
+ import dev .sigstore .rekor .dsse .v0_0_1 .Dsse ;
39
+ import dev .sigstore .rekor .dsse .v0_0_1 .PayloadHash ;
36
40
import dev .sigstore .tuf .SigstoreTufClient ;
37
41
import java .io .IOException ;
38
42
import java .nio .charset .StandardCharsets ;
52
56
import java .util .List ;
53
57
import java .util .Objects ;
54
58
import java .util .stream .Collectors ;
59
+ import org .bouncycastle .util .encoders .DecoderException ;
55
60
import org .bouncycastle .util .encoders .Hex ;
56
61
57
62
/** Verify hashrekords from rekor signed using the keyless signing flow with fulcio certificates. */
@@ -125,12 +130,9 @@ public void verify(Path artifact, Bundle bundle, VerificationOptions options)
125
130
public void verify (byte [] artifactDigest , Bundle bundle , VerificationOptions options )
126
131
throws KeylessVerificationException {
127
132
128
- if (bundle .getDsseEnvelope ().isPresent ()) {
129
- throw new KeylessVerificationException ("Cannot verify DSSE signature based bundles" );
130
- }
131
- if (bundle .getMessageSignature ().isEmpty ()) {
132
- // this should be unreachable
133
- throw new IllegalStateException ("Bundle must contain a message signature to verify" );
133
+ if (bundle .getDsseEnvelope ().isEmpty () && bundle .getMessageSignature ().isEmpty ()) {
134
+ throw new IllegalStateException (
135
+ "Bundle must contain a message signature or DSSE envelope to verify" );
134
136
}
135
137
136
138
if (bundle .getEntries ().isEmpty ()) {
@@ -182,7 +184,12 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
182
184
throw new KeylessVerificationException ("Signing time was after certificate expiry" , e );
183
185
}
184
186
185
- checkMessageSignature (bundle .getMessageSignature ().get (), rekorEntry , artifactDigest , leafCert );
187
+ if (bundle .getMessageSignature ().isPresent ()) { // hashedrekord
188
+ checkMessageSignature (
189
+ bundle .getMessageSignature ().get (), rekorEntry , artifactDigest , leafCert );
190
+ } else { // dsse
191
+ checkDsseEnvelope (rekorEntry , bundle .getDsseEnvelope ().get (), artifactDigest , leafCert );
192
+ }
186
193
}
187
194
188
195
@ VisibleForTesting
@@ -201,7 +208,7 @@ void checkCertificateMatchers(X509Certificate cert, List<CertificateMatcher> mat
201
208
}
202
209
}
203
210
204
- void checkMessageSignature (
211
+ private void checkMessageSignature (
205
212
MessageSignature messageSignature ,
206
213
RekorEntry rekorEntry ,
207
214
byte [] artifactDigest ,
@@ -255,4 +262,110 @@ void checkMessageSignature(
255
262
throw new KeylessVerificationException ("Unexpected rekor type" , re );
256
263
}
257
264
}
265
+
266
+ // do all dsse specific checks
267
+ private void checkDsseEnvelope (
268
+ RekorEntry rekorEntry ,
269
+ DsseEnvelope dsseEnvelope ,
270
+ byte [] artifactDigest ,
271
+ X509Certificate leafCert )
272
+ throws KeylessVerificationException {
273
+
274
+ // verify the artifact is in the subject list of the envelope
275
+ if (!Objects .equals (InTotoPayload .PAYLOAD_TYPE , dsseEnvelope .getPayloadType ())) {
276
+ throw new KeylessVerificationException (
277
+ "DSSE envelope must have payload type "
278
+ + InTotoPayload .PAYLOAD_TYPE
279
+ + ", but found '"
280
+ + dsseEnvelope .getPayloadType ()
281
+ + "'" );
282
+ }
283
+ InTotoPayload payload = InTotoPayload .from (dsseEnvelope );
284
+
285
+ // find one sha256 hash in the subject list that matches the artifact hash
286
+ if (payload .getSubject ().stream ()
287
+ .noneMatch (
288
+ subject -> {
289
+ if (subject .getDigest ().containsKey ("sha256" )) {
290
+ try {
291
+ var digestBytes = Hex .decode (subject .getDigest ().get ("sha256" ));
292
+ return Arrays .equals (artifactDigest , digestBytes );
293
+ } catch (DecoderException de ) {
294
+ // ignore (assume false)
295
+ }
296
+ }
297
+ return false ;
298
+ })) {
299
+ var providedHashes =
300
+ payload .getSubject ().stream ()
301
+ .map (s -> s .getDigest ().getOrDefault ("sha256" , "no-sha256-hash" ))
302
+ .collect (Collectors .joining ("," , "[" , "]" ));
303
+
304
+ throw new KeylessVerificationException (
305
+ "Provided artifact digest does not match any subject sha256 digests in DSSE payload"
306
+ + "\n provided(hex) : "
307
+ + Hex .toHexString (artifactDigest )
308
+ + "\n verification : "
309
+ + providedHashes );
310
+ }
311
+
312
+ // verify the dsse signature
313
+ if (dsseEnvelope .getSignatures ().size () != 1 ) {
314
+ throw new KeylessVerificationException (
315
+ "DSSE envelope must have exactly 1 signature, but found: "
316
+ + dsseEnvelope .getSignatures ().size ());
317
+ }
318
+ try {
319
+ if (!Verifiers .newVerifier (leafCert .getPublicKey ())
320
+ .verify (dsseEnvelope .getPAE (), dsseEnvelope .getSignature ())) {
321
+ throw new KeylessVerificationException ("DSSE signature was not valid" );
322
+ }
323
+ } catch (NoSuchAlgorithmException | InvalidKeyException ex ) {
324
+ throw new RuntimeException (ex );
325
+ } catch (SignatureException se ) {
326
+ throw new KeylessVerificationException ("Signature could not be processed" , se );
327
+ }
328
+
329
+ // check if the digest over the dsse payload matches the digest in the rekorEntry
330
+ Dsse rekorDsse ;
331
+ try {
332
+ rekorDsse = RekorTypes .getDsse (rekorEntry );
333
+ } catch (RekorTypeException re ) {
334
+ throw new KeylessVerificationException ("Unexpected rekor type" , re );
335
+ }
336
+
337
+ var algorithm = rekorDsse .getPayloadHash ().getAlgorithm ();
338
+ if (algorithm != PayloadHash .Algorithm .SHA_256 ) {
339
+ throw new KeylessVerificationException (
340
+ "Cannot process DSSE entry with hashing algorithm " + algorithm .toString ());
341
+ }
342
+
343
+ byte [] payloadDigest ;
344
+ try {
345
+ payloadDigest = Hex .decode (rekorDsse .getPayloadHash ().getValue ());
346
+ } catch (DecoderException de ) {
347
+ throw new KeylessVerificationException (
348
+ "Could not decode hex sha256 artifact hash in hashrekord" , de );
349
+ }
350
+
351
+ byte [] calculatedDigest = Hashing .sha256 ().hashBytes (dsseEnvelope .getPayload ()).asBytes ();
352
+ if (!Arrays .equals (calculatedDigest , payloadDigest )) {
353
+ throw new KeylessVerificationException (
354
+ "Digest of DSSE payload in bundle does not match DSSE payload digest in log entry" );
355
+ }
356
+
357
+ // check if the signature over the dsse payload matches the signature in the rekorEntry
358
+ if (rekorDsse .getSignatures ().size () != 1 ) {
359
+ throw new KeylessVerificationException (
360
+ "DSSE log entry must have exactly 1 signature, but found: "
361
+ + rekorDsse .getSignatures ().size ());
362
+ }
363
+
364
+ if (!Base64 .getEncoder ()
365
+ .encodeToString (dsseEnvelope .getSignature ())
366
+ .equals (rekorDsse .getSignatures ().get (0 ).getSignature ())) {
367
+ throw new KeylessVerificationException (
368
+ "Provided DSSE signature materials are inconsistent with DSSE log entry" );
369
+ }
370
+ }
258
371
}
0 commit comments