22
22
import com .google .errorprone .annotations .CanIgnoreReturnValue ;
23
23
import com .google .errorprone .annotations .CheckReturnValue ;
24
24
import com .google .errorprone .annotations .concurrent .GuardedBy ;
25
+ import com .google .protobuf .ByteString ;
25
26
import dev .sigstore .bundle .Bundle ;
26
27
import dev .sigstore .bundle .Bundle .HashAlgorithm ;
27
28
import dev .sigstore .bundle .Bundle .MessageSignature ;
40
41
import dev .sigstore .oidc .client .OidcException ;
41
42
import dev .sigstore .oidc .client .OidcToken ;
42
43
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 ;
43
49
import dev .sigstore .rekor .client .HashedRekordRequest ;
44
50
import dev .sigstore .rekor .client .RekorClient ;
45
51
import dev .sigstore .rekor .client .RekorClientHttp ;
52
+ import dev .sigstore .rekor .client .RekorEntry ;
46
53
import dev .sigstore .rekor .client .RekorParseException ;
47
54
import dev .sigstore .rekor .client .RekorResponse ;
48
55
import dev .sigstore .rekor .client .RekorVerificationException ;
49
56
import dev .sigstore .rekor .client .RekorVerifier ;
57
+ import dev .sigstore .rekor .v2 .client .RekorV2Client ;
58
+ import dev .sigstore .rekor .v2 .client .RekorV2ClientHttp ;
50
59
import dev .sigstore .timestamp .client .ImmutableTimestampRequest ;
51
60
import dev .sigstore .timestamp .client .TimestampClient ;
52
61
import dev .sigstore .timestamp .client .TimestampClientHttp ;
69
78
import java .security .cert .CertificateException ;
70
79
import java .security .spec .InvalidKeySpecException ;
71
80
import java .time .Duration ;
81
+ import java .time .Instant ;
72
82
import java .util .ArrayList ;
73
83
import java .util .Collections ;
74
84
import java .util .List ;
@@ -97,6 +107,7 @@ public class KeylessSigner implements AutoCloseable {
97
107
private final FulcioClient fulcioClient ;
98
108
private final FulcioVerifier fulcioVerifier ;
99
109
private final RekorClient rekorClient ;
110
+ private final RekorV2Client rekorV2Client ;
100
111
private final RekorVerifier rekorVerifier ;
101
112
private final TimestampClient timestampClient ;
102
113
private final TimestampVerifier timestampVerifier ;
@@ -111,19 +122,28 @@ public class KeylessSigner implements AutoCloseable {
111
122
private CertPath signingCert ;
112
123
113
124
/**
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.
116
127
*/
117
128
@ GuardedBy ("lock" )
118
129
@ Nullable
119
130
private byte [] signingCertPemBytes ;
120
131
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
+
121
140
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock ();
122
141
123
142
private KeylessSigner (
124
143
FulcioClient fulcioClient ,
125
144
FulcioVerifier fulcioVerifier ,
126
145
RekorClient rekorClient ,
146
+ RekorV2Client rekorV2Client ,
127
147
RekorVerifier rekorVerifier ,
128
148
TimestampClient timestampClient ,
129
149
TimestampVerifier timestampVerifier ,
@@ -134,6 +154,7 @@ private KeylessSigner(
134
154
this .fulcioClient = fulcioClient ;
135
155
this .fulcioVerifier = fulcioVerifier ;
136
156
this .rekorClient = rekorClient ;
157
+ this .rekorV2Client = rekorV2Client ;
137
158
this .rekorVerifier = rekorVerifier ;
138
159
this .timestampClient = timestampClient ;
139
160
this .timestampVerifier = timestampVerifier ;
@@ -149,6 +170,7 @@ public void close() {
149
170
try {
150
171
signingCert = null ;
151
172
signingCertPemBytes = null ;
173
+ encodedCert = null ;
152
174
} finally {
153
175
lock .writeLock ().unlock ();
154
176
}
@@ -166,6 +188,7 @@ public static class Builder {
166
188
private List <OidcTokenMatcher > oidcIdentities = Collections .emptyList ();
167
189
private Signer signer ;
168
190
private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME ;
191
+ private boolean enableRekorV2 = false ;
169
192
170
193
@ CanIgnoreReturnValue
171
194
public Builder trustedRootProvider (TrustedRootProvider trustedRootProvider ) {
@@ -179,6 +202,12 @@ public Builder signingConfigProvider(SigningConfigProvider signingConfigProvider
179
202
return this ;
180
203
}
181
204
205
+ @ CanIgnoreReturnValue
206
+ public Builder enableRekorV2 (boolean enableRekorV2 ) {
207
+ this .enableRekorV2 = enableRekorV2 ;
208
+ return this ;
209
+ }
210
+
182
211
/**
183
212
* Deprecated, use {@link #forceCredentialProviders}. sigstore-gradle requires a one version
184
213
* deprecation window, so keep this in here until we've done another release.
@@ -257,12 +286,22 @@ public KeylessSigner build()
257
286
var fulcioClient = FulcioClientGrpc .builder ().setService (fulcioService .get ()).build ();
258
287
var fulcioVerifier = FulcioVerifier .newFulcioVerifier (trustedRoot );
259
288
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 ));
261
291
if (rekorService .isEmpty ()) {
262
292
throw new SigstoreConfigurationException (
263
293
"No suitable rekor target was found in signing config" );
264
294
}
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
+
266
305
var rekorVerifier = RekorVerifier .newRekorVerifier (trustedRoot );
267
306
268
307
TimestampClient timestampClient = null ;
@@ -293,6 +332,7 @@ public KeylessSigner build()
293
332
fulcioClient ,
294
333
fulcioVerifier ,
295
334
rekorClient ,
335
+ rekorV2Client ,
296
336
rekorVerifier ,
297
337
timestampClient ,
298
338
timestampVerifier ,
@@ -378,49 +418,31 @@ public List<Bundle> sign(List<byte[]> artifactDigests) throws KeylessSignerExcep
378
418
379
419
CertPath signingCert ;
380
420
byte [] signingCertPemBytes ;
421
+ byte [] encodedCert ;
381
422
lock .readLock ().lock ();
382
423
try {
383
424
signingCert = this .signingCert ;
384
425
signingCertPemBytes = this .signingCertPemBytes ;
426
+ encodedCert = this .encodedCert ;
385
427
if (signingCert == null ) {
386
428
throw new IllegalStateException ("Signing certificate is null" );
387
429
}
388
430
} finally {
389
431
lock .readLock ().unlock ();
390
432
}
391
433
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
-
415
434
var bundleBuilder =
416
435
ImmutableBundle .builder ()
417
436
.certPath (signingCert )
418
- .addEntries (rekorResponse .getEntry ())
419
437
.messageSignature (
420
438
MessageSignature .of (HashAlgorithm .SHA2_256 , artifactDigest , signature ));
421
439
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
+
424
446
var signatureDigest = Hashing .sha256 ().hashBytes (signature ).asBytes ();
425
447
426
448
var tsReq =
@@ -446,6 +468,74 @@ public List<Bundle> sign(List<byte[]> artifactDigests) throws KeylessSignerExcep
446
468
ImmutableTimestamp .builder ().rfc3161Timestamp (tsResp .getEncoded ()).build ();
447
469
448
470
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." );
449
539
}
450
540
451
541
result .add (bundleBuilder .build ());
@@ -485,6 +575,7 @@ private void renewSigningCertificate()
485
575
try {
486
576
signingCert = null ;
487
577
signingCertPemBytes = null ;
578
+ encodedCert = null ;
488
579
OidcToken tokenInfo = oidcClients .getIDToken ();
489
580
490
581
// check if we have an allow list and if so, ensure the provided token is in there
@@ -510,6 +601,7 @@ private void renewSigningCertificate()
510
601
fulcioVerifier .verifySigningCertificate (trimmed );
511
602
this .signingCert = trimmed ;
512
603
signingCertPemBytes = Certificates .toPemBytes (signingCert );
604
+ encodedCert = Certificates .getLeaf (signingCert ).getEncoded ();
513
605
} finally {
514
606
lock .writeLock ().unlock ();
515
607
}
0 commit comments