|
23 | 23 | import com.google.errorprone.annotations.CheckReturnValue; |
24 | 24 | import com.google.errorprone.annotations.concurrent.GuardedBy; |
25 | 25 | import com.google.protobuf.ByteString; |
26 | | -import dev.sigstore.bundle.Bundle; |
| 26 | +import com.google.protobuf.InvalidProtocolBufferException; |
| 27 | +import com.google.protobuf.util.JsonFormat; |
| 28 | +import dev.sigstore.bundle.*; |
27 | 29 | import dev.sigstore.bundle.Bundle.MessageSignature; |
28 | | -import dev.sigstore.bundle.ImmutableBundle; |
29 | | -import dev.sigstore.bundle.ImmutableTimestamp; |
30 | 30 | import dev.sigstore.encryption.certificates.Certificates; |
31 | 31 | import dev.sigstore.encryption.signers.Signer; |
32 | 32 | import dev.sigstore.encryption.signers.Signers; |
|
42 | 42 | import dev.sigstore.oidc.client.OidcTokenMatcher; |
43 | 43 | import dev.sigstore.proto.ProtoMutators; |
44 | 44 | import dev.sigstore.proto.common.v1.X509Certificate; |
| 45 | +import dev.sigstore.proto.rekor.v2.DSSERequestV002; |
45 | 46 | import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002; |
46 | 47 | import dev.sigstore.proto.rekor.v2.Signature; |
47 | 48 | import dev.sigstore.proto.rekor.v2.Verifier; |
|
66 | 67 | import dev.sigstore.trustroot.Service; |
67 | 68 | import dev.sigstore.trustroot.SigstoreConfigurationException; |
68 | 69 | import dev.sigstore.tuf.SigstoreTufClient; |
| 70 | +import io.intoto.EnvelopeOuterClass; |
69 | 71 | import java.io.IOException; |
70 | 72 | import java.nio.charset.StandardCharsets; |
71 | 73 | import java.nio.file.Path; |
@@ -383,6 +385,140 @@ public Builder sigstoreStagingDefaults() { |
383 | 385 | } |
384 | 386 | } |
385 | 387 |
|
| 388 | + public Bundle attest(String payload) throws KeylessSignerException { |
| 389 | + // Technically speaking, it is unlikely the certificate will expire between signing artifacts |
| 390 | + // However, files might be large, and it might take time to talk to Rekor |
| 391 | + // so we check the certificate expiration here. |
| 392 | + try { |
| 393 | + renewSigningCertificate(); |
| 394 | + } catch (FulcioVerificationException |
| 395 | + | UnsupportedAlgorithmException |
| 396 | + | OidcException |
| 397 | + | IOException |
| 398 | + | InterruptedException |
| 399 | + | InvalidKeyException |
| 400 | + | NoSuchAlgorithmException |
| 401 | + | SignatureException |
| 402 | + | CertificateException ex) { |
| 403 | + throw new KeylessSignerException("Failed to obtain signing certificate", ex); |
| 404 | + } |
| 405 | + CertPath signingCert; |
| 406 | + byte[] signingCertPemBytes; |
| 407 | + byte[] encodedCert; |
| 408 | + lock.readLock().lock(); |
| 409 | + try { |
| 410 | + signingCert = this.signingCert; |
| 411 | + signingCertPemBytes = this.signingCertPemBytes; |
| 412 | + encodedCert = this.encodedCert; |
| 413 | + if (signingCert == null) { |
| 414 | + throw new IllegalStateException("Signing certificate is null"); |
| 415 | + } |
| 416 | + } finally { |
| 417 | + lock.readLock().unlock(); |
| 418 | + } |
| 419 | + |
| 420 | + var bundleBuilder = ImmutableBundle.builder().certPath(signingCert); |
| 421 | + |
| 422 | + if (rekorV2Client != null) { // Using Rekor v2 and a TSA |
| 423 | + Preconditions.checkNotNull( |
| 424 | + timestampClient, "Timestamp client must be configured for Rekor v2"); |
| 425 | + Preconditions.checkNotNull( |
| 426 | + timestampVerifier, "Timestamp verifier must be configured for Rekor v2"); |
| 427 | + |
| 428 | + var verifier = |
| 429 | + Verifier.newBuilder() |
| 430 | + .setX509Certificate( |
| 431 | + X509Certificate.newBuilder() |
| 432 | + .setRawBytes(ByteString.copyFrom(encodedCert)) |
| 433 | + .build()) |
| 434 | + .setKeyDetails(ProtoMutators.toPublicKeyDetails(signingAlgorithm)) |
| 435 | + .build(); |
| 436 | + |
| 437 | + var dsse = |
| 438 | + ImmutableDsseEnvelope.builder() |
| 439 | + .payload(payload.getBytes(StandardCharsets.UTF_8)) |
| 440 | + .payloadType("application/vnd.in-toto+json") |
| 441 | + .build(); |
| 442 | + var pae = dsse.getPAE(); |
| 443 | + Bundle.DsseEnvelope dsseSigned; |
| 444 | + try { |
| 445 | + var sig = signer.sign(pae); |
| 446 | + dsseSigned = |
| 447 | + ImmutableDsseEnvelope.builder() |
| 448 | + .from(dsse) |
| 449 | + .addSignatures(ImmutableSignature.builder().sig(sig).build()) |
| 450 | + .build(); |
| 451 | + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { |
| 452 | + throw new RuntimeException(e); |
| 453 | + } |
| 454 | + |
| 455 | + var dsseRequest = |
| 456 | + DSSERequestV002.newBuilder() |
| 457 | + .setEnvelope( |
| 458 | + EnvelopeOuterClass.Envelope.newBuilder() |
| 459 | + .setPayload(ByteString.copyFrom(dsseSigned.getPayload())) |
| 460 | + .setPayloadType(dsseSigned.getPayloadType()) |
| 461 | + .addSignatures( |
| 462 | + EnvelopeOuterClass.Signature.newBuilder() |
| 463 | + .setSig(ByteString.copyFrom(dsseSigned.getSignature()))) |
| 464 | + .build()) |
| 465 | + .addVerifiers(verifier) |
| 466 | + .build(); |
| 467 | + |
| 468 | + try { |
| 469 | + System.out.println(JsonFormat.printer().print(dsseRequest)); |
| 470 | + } catch (InvalidProtocolBufferException e) { |
| 471 | + throw new RuntimeException(e); |
| 472 | + } |
| 473 | + |
| 474 | + var signatureDigest = Hashing.sha256().hashBytes(dsseSigned.getSignature()).asBytes(); |
| 475 | + |
| 476 | + var tsReq = |
| 477 | + ImmutableTimestampRequest.builder() |
| 478 | + .hashAlgorithm(dev.sigstore.timestamp.client.HashAlgorithm.SHA256) |
| 479 | + .hash(signatureDigest) |
| 480 | + .build(); |
| 481 | + |
| 482 | + TimestampResponse tsResp; |
| 483 | + try { |
| 484 | + tsResp = timestampClient.timestamp(tsReq); |
| 485 | + } catch (TimestampException ex) { |
| 486 | + throw new KeylessSignerException("Failed to generate timestamp", ex); |
| 487 | + } |
| 488 | + |
| 489 | + try { |
| 490 | + timestampVerifier.verify(tsResp, dsseSigned.getSignature()); |
| 491 | + } catch (TimestampVerificationException ex) { |
| 492 | + throw new KeylessSignerException("Returned timestamp was invalid", ex); |
| 493 | + } |
| 494 | + |
| 495 | + Bundle.Timestamp timestamp = |
| 496 | + ImmutableTimestamp.builder().rfc3161Timestamp(tsResp.getEncoded()).build(); |
| 497 | + |
| 498 | + bundleBuilder.addTimestamps(timestamp); |
| 499 | + |
| 500 | + RekorEntry entry; |
| 501 | + try { |
| 502 | + entry = rekorV2Client.putEntry(dsseRequest); |
| 503 | + } catch (IOException | RekorParseException ex) { |
| 504 | + throw new KeylessSignerException("Failed to put entry in rekor", ex); |
| 505 | + } |
| 506 | + |
| 507 | + try { |
| 508 | + rekorVerifier.verifyEntry(entry); |
| 509 | + } catch (RekorVerificationException ex) { |
| 510 | + throw new KeylessSignerException("Failed to validate rekor entry after signing", ex); |
| 511 | + } |
| 512 | + |
| 513 | + bundleBuilder.dsseEnvelope(dsseSigned); |
| 514 | + |
| 515 | + bundleBuilder.addEntries(entry); |
| 516 | + } else { |
| 517 | + throw new IllegalStateException("Rekor v2 client was not configured."); |
| 518 | + } |
| 519 | + return bundleBuilder.build(); |
| 520 | + } |
| 521 | + |
386 | 522 | /** |
387 | 523 | * Sign one or more artifact digests using the keyless signing workflow. The oidc/fulcio dance to |
388 | 524 | * obtain a signing certificate will only occur once. The same ephemeral private key will be used |
|
0 commit comments