Skip to content

Commit 4b7a49e

Browse files
authored
Merge pull request #1008 from sigstore/rekor-v2-keyless
Add Rekor v2 support to keyless signer and verifier
2 parents ae74a3c + a6f5cec commit 4b7a49e

File tree

4 files changed

+578
-206
lines changed

4 files changed

+578
-206
lines changed

sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java

Lines changed: 122 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2323
import com.google.errorprone.annotations.CheckReturnValue;
2424
import com.google.errorprone.annotations.concurrent.GuardedBy;
25+
import com.google.protobuf.ByteString;
2526
import dev.sigstore.bundle.Bundle;
2627
import dev.sigstore.bundle.Bundle.HashAlgorithm;
2728
import dev.sigstore.bundle.Bundle.MessageSignature;
@@ -40,13 +41,21 @@
4041
import dev.sigstore.oidc.client.OidcException;
4142
import dev.sigstore.oidc.client.OidcToken;
4243
import dev.sigstore.oidc.client.OidcTokenMatcher;
44+
import dev.sigstore.proto.common.v1.PublicKeyDetails;
45+
import dev.sigstore.proto.common.v1.X509Certificate;
46+
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
47+
import dev.sigstore.proto.rekor.v2.Signature;
48+
import dev.sigstore.proto.rekor.v2.Verifier;
4349
import dev.sigstore.rekor.client.HashedRekordRequest;
4450
import dev.sigstore.rekor.client.RekorClient;
4551
import dev.sigstore.rekor.client.RekorClientHttp;
52+
import dev.sigstore.rekor.client.RekorEntry;
4653
import dev.sigstore.rekor.client.RekorParseException;
4754
import dev.sigstore.rekor.client.RekorResponse;
4855
import dev.sigstore.rekor.client.RekorVerificationException;
4956
import dev.sigstore.rekor.client.RekorVerifier;
57+
import dev.sigstore.rekor.v2.client.RekorV2Client;
58+
import dev.sigstore.rekor.v2.client.RekorV2ClientHttp;
5059
import dev.sigstore.timestamp.client.ImmutableTimestampRequest;
5160
import dev.sigstore.timestamp.client.TimestampClient;
5261
import dev.sigstore.timestamp.client.TimestampClientHttp;
@@ -69,6 +78,7 @@
6978
import java.security.cert.CertificateException;
7079
import java.security.spec.InvalidKeySpecException;
7180
import java.time.Duration;
81+
import java.time.Instant;
7282
import java.util.ArrayList;
7383
import java.util.Collections;
7484
import java.util.List;
@@ -97,6 +107,7 @@ public class KeylessSigner implements AutoCloseable {
97107
private final FulcioClient fulcioClient;
98108
private final FulcioVerifier fulcioVerifier;
99109
private final RekorClient rekorClient;
110+
private final RekorV2Client rekorV2Client;
100111
private final RekorVerifier rekorVerifier;
101112
private final TimestampClient timestampClient;
102113
private final TimestampVerifier timestampVerifier;
@@ -111,19 +122,28 @@ public class KeylessSigner implements AutoCloseable {
111122
private CertPath signingCert;
112123

113124
/**
114-
* Representation {@link #signingCert} in PEM bytes format. This is used to avoid serializing the
115-
* certificate for each use.
125+
* Representation of {@link #signingCert} in PEM bytes format. This is used to avoid serializing
126+
* the certificate for each use.
116127
*/
117128
@GuardedBy("lock")
118129
@Nullable
119130
private byte[] signingCertPemBytes;
120131

132+
/**
133+
* Representation of {@link #signingCert} in DER encoded bytes. his is used to avoid serializing
134+
* the certificate for each use.
135+
*/
136+
@GuardedBy("lock")
137+
@Nullable
138+
private byte[] encodedCert;
139+
121140
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
122141

123142
private KeylessSigner(
124143
FulcioClient fulcioClient,
125144
FulcioVerifier fulcioVerifier,
126145
RekorClient rekorClient,
146+
RekorV2Client rekorV2Client,
127147
RekorVerifier rekorVerifier,
128148
TimestampClient timestampClient,
129149
TimestampVerifier timestampVerifier,
@@ -134,6 +154,7 @@ private KeylessSigner(
134154
this.fulcioClient = fulcioClient;
135155
this.fulcioVerifier = fulcioVerifier;
136156
this.rekorClient = rekorClient;
157+
this.rekorV2Client = rekorV2Client;
137158
this.rekorVerifier = rekorVerifier;
138159
this.timestampClient = timestampClient;
139160
this.timestampVerifier = timestampVerifier;
@@ -149,6 +170,7 @@ public void close() {
149170
try {
150171
signingCert = null;
151172
signingCertPemBytes = null;
173+
encodedCert = null;
152174
} finally {
153175
lock.writeLock().unlock();
154176
}
@@ -166,6 +188,7 @@ public static class Builder {
166188
private List<OidcTokenMatcher> oidcIdentities = Collections.emptyList();
167189
private Signer signer;
168190
private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME;
191+
private boolean enableRekorV2 = false;
169192

170193
@CanIgnoreReturnValue
171194
public Builder trustedRootProvider(TrustedRootProvider trustedRootProvider) {
@@ -179,6 +202,12 @@ public Builder signingConfigProvider(SigningConfigProvider signingConfigProvider
179202
return this;
180203
}
181204

205+
@CanIgnoreReturnValue
206+
public Builder enableRekorV2(boolean enableRekorV2) {
207+
this.enableRekorV2 = enableRekorV2;
208+
return this;
209+
}
210+
182211
/**
183212
* Deprecated, use {@link #forceCredentialProviders}. sigstore-gradle requires a one version
184213
* deprecation window, so keep this in here until we've done another release.
@@ -257,12 +286,22 @@ public KeylessSigner build()
257286
var fulcioClient = FulcioClientGrpc.builder().setService(fulcioService.get()).build();
258287
var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot);
259288

260-
var rekorService = Service.select(signingConfig.getTLogs(), List.of(1));
289+
var rekorService =
290+
Service.select(signingConfig.getTLogs(), enableRekorV2 ? List.of(1) : List.of(1, 2));
261291
if (rekorService.isEmpty()) {
262292
throw new SigstoreConfigurationException(
263293
"No suitable rekor target was found in signing config");
264294
}
265-
var rekorClient = RekorClientHttp.builder().setService(rekorService.get()).build();
295+
296+
RekorClient rekorClient = null;
297+
RekorV2Client rekorV2Client = null;
298+
299+
if (rekorService.get().getApiVersion() == 1) {
300+
rekorClient = RekorClientHttp.builder().setService(rekorService.get()).build();
301+
} else {
302+
rekorV2Client = RekorV2ClientHttp.builder().setService(rekorService.get()).build();
303+
}
304+
266305
var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot);
267306

268307
TimestampClient timestampClient = null;
@@ -293,6 +332,7 @@ public KeylessSigner build()
293332
fulcioClient,
294333
fulcioVerifier,
295334
rekorClient,
335+
rekorV2Client,
296336
rekorVerifier,
297337
timestampClient,
298338
timestampVerifier,
@@ -378,49 +418,31 @@ public List<Bundle> sign(List<byte[]> artifactDigests) throws KeylessSignerExcep
378418

379419
CertPath signingCert;
380420
byte[] signingCertPemBytes;
421+
byte[] encodedCert;
381422
lock.readLock().lock();
382423
try {
383424
signingCert = this.signingCert;
384425
signingCertPemBytes = this.signingCertPemBytes;
426+
encodedCert = this.encodedCert;
385427
if (signingCert == null) {
386428
throw new IllegalStateException("Signing certificate is null");
387429
}
388430
} finally {
389431
lock.readLock().unlock();
390432
}
391433

392-
var rekorRequest =
393-
HashedRekordRequest.newHashedRekordRequest(
394-
artifactDigest, signingCertPemBytes, signature);
395-
396-
RekorResponse rekorResponse;
397-
try {
398-
rekorResponse = rekorClient.putEntry(rekorRequest);
399-
} catch (RekorParseException | IOException ex) {
400-
throw new KeylessSignerException("Failed to put entry in rekor", ex);
401-
}
402-
403-
var calculatedHashedRekord =
404-
Base64.toBase64String(rekorRequest.toJsonPayload().getBytes(StandardCharsets.UTF_8));
405-
if (!Objects.equals(calculatedHashedRekord, rekorResponse.getEntry().getBody())) {
406-
throw new KeylessSignerException("Returned log entry was inconsistent with request");
407-
}
408-
409-
try {
410-
rekorVerifier.verifyEntry(rekorResponse.getEntry());
411-
} catch (RekorVerificationException ex) {
412-
throw new KeylessSignerException("Failed to validate rekor response after signing", ex);
413-
}
414-
415434
var bundleBuilder =
416435
ImmutableBundle.builder()
417436
.certPath(signingCert)
418-
.addEntries(rekorResponse.getEntry())
419437
.messageSignature(
420438
MessageSignature.of(HashAlgorithm.SHA2_256, artifactDigest, signature));
421439

422-
// Timestamp functionality only enabled if timestampUri is provided
423-
if (timestampClient != null && timestampVerifier != null) {
440+
if (rekorV2Client != null) { // Using Rekor v2 and a TSA
441+
Preconditions.checkNotNull(
442+
timestampClient, "Timestamp client must be configured for Rekor v2");
443+
Preconditions.checkNotNull(
444+
timestampVerifier, "Timestamp verifier must be configured for Rekor v2");
445+
424446
var signatureDigest = Hashing.sha256().hashBytes(signature).asBytes();
425447

426448
var tsReq =
@@ -446,6 +468,74 @@ public List<Bundle> sign(List<byte[]> artifactDigests) throws KeylessSignerExcep
446468
ImmutableTimestamp.builder().rfc3161Timestamp(tsResp.getEncoded()).build();
447469

448470
bundleBuilder.addTimestamps(timestamp);
471+
472+
var verifier =
473+
Verifier.newBuilder()
474+
.setX509Certificate(
475+
X509Certificate.newBuilder()
476+
.setRawBytes(ByteString.copyFrom(encodedCert))
477+
.build())
478+
.setKeyDetails(PublicKeyDetails.PKIX_ECDSA_P256_SHA_256)
479+
.build();
480+
481+
var reqSignature =
482+
Signature.newBuilder()
483+
.setContent(ByteString.copyFrom(signature))
484+
.setVerifier(verifier)
485+
.build();
486+
487+
var hashedRekordRequest =
488+
HashedRekordRequestV002.newBuilder()
489+
.setDigest(ByteString.copyFrom(artifactDigest))
490+
.setSignature(reqSignature)
491+
.build();
492+
493+
RekorEntry entry;
494+
try {
495+
entry = rekorV2Client.putEntry(hashedRekordRequest);
496+
} catch (IOException | RekorParseException ex) {
497+
throw new KeylessSignerException("Failed to put entry in rekor", ex);
498+
}
499+
500+
try {
501+
List<Instant> timestamps = new ArrayList<>();
502+
timestamps.add(tsResp.getGenTime().toInstant());
503+
if (entry.getIntegratedTime() != 0) {
504+
timestamps.add(entry.getIntegratedTimeInstant());
505+
}
506+
rekorVerifier.verifyEntry(entry);
507+
} catch (RekorVerificationException | TimestampException ex) {
508+
throw new KeylessSignerException("Failed to validate rekor entry after signing", ex);
509+
}
510+
511+
bundleBuilder.addEntries(entry);
512+
} else if (rekorClient != null) { // Using Rekor v1
513+
var rekorRequest =
514+
HashedRekordRequest.newHashedRekordRequest(
515+
artifactDigest, signingCertPemBytes, signature);
516+
517+
RekorResponse rekorResponse;
518+
try {
519+
rekorResponse = rekorClient.putEntry(rekorRequest);
520+
} catch (RekorParseException | IOException ex) {
521+
throw new KeylessSignerException("Failed to put entry in rekor", ex);
522+
}
523+
524+
var calculatedHashedRekord =
525+
Base64.toBase64String(rekorRequest.toJsonPayload().getBytes(StandardCharsets.UTF_8));
526+
if (!Objects.equals(calculatedHashedRekord, rekorResponse.getEntry().getBody())) {
527+
throw new KeylessSignerException("Returned log entry was inconsistent with request");
528+
}
529+
530+
try {
531+
rekorVerifier.verifyEntry(rekorResponse.getEntry());
532+
} catch (RekorVerificationException ex) {
533+
throw new KeylessSignerException("Failed to validate rekor response after signing", ex);
534+
}
535+
536+
bundleBuilder.addEntries(rekorResponse.getEntry());
537+
} else {
538+
throw new IllegalStateException("No rekor client was configured.");
449539
}
450540

451541
result.add(bundleBuilder.build());
@@ -485,6 +575,7 @@ private void renewSigningCertificate()
485575
try {
486576
signingCert = null;
487577
signingCertPemBytes = null;
578+
encodedCert = null;
488579
OidcToken tokenInfo = oidcClients.getIDToken();
489580

490581
// check if we have an allow list and if so, ensure the provided token is in there
@@ -510,6 +601,7 @@ private void renewSigningCertificate()
510601
fulcioVerifier.verifySigningCertificate(trimmed);
511602
this.signingCert = trimmed;
512603
signingCertPemBytes = Certificates.toPemBytes(signingCert);
604+
encodedCert = Certificates.getLeaf(signingCert).getEncoded();
513605
} finally {
514606
lock.writeLock().unlock();
515607
}

0 commit comments

Comments
 (0)