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