|
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
|
@@ -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 | + 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 | + + "\nprovided(hex) : " |
| 307 | + + Hex.toHexString(artifactDigest) |
| 308 | + + "\nverification : " |
| 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