Skip to content

Commit 80470a7

Browse files
authored
Merge pull request #1252 from anthonydahanne/issue-1251-keylessverifier-private
Close #1251: Allow no transparency-log entries
2 parents a4ea3fc + 400f676 commit 80470a7

9 files changed

Lines changed: 305 additions & 49 deletions

File tree

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

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -132,31 +132,30 @@ public Builder trustedRootProvider(TrustedRootProvider trustedRootProvider) {
132132
private AlgorithmRegistry.HashAlgorithm findHashAlgorithm(Bundle bundle)
133133
throws KeylessVerificationException {
134134
try {
135-
var rekorEntry = bundle.getEntries().get(0);
136-
var body = rekorEntry.getBodyDecoded();
137-
if ("0.0.1".equals(body.getApiVersion())) {
138-
if ("hashedrekord".equals(body.getKind())) {
139-
var entry = RekorTypes.getHashedRekordV001(rekorEntry);
140-
switch (entry.getData().getHash().getAlgorithm()) {
141-
case SHA_256:
142-
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
143-
case SHA_384:
144-
return AlgorithmRegistry.HashAlgorithm.SHA2_384;
145-
case SHA_512:
146-
return AlgorithmRegistry.HashAlgorithm.SHA2_512;
135+
// special case hashedrekord:0.0.1 and dsse:0.0.1 which may contain legacy public key to hash
136+
// algorithm mappings
137+
if (!bundle.getEntries().isEmpty()) {
138+
var rekorEntry = bundle.getEntries().get(0);
139+
var body = rekorEntry.getBodyDecoded();
140+
if ("0.0.1".equals(body.getApiVersion())) {
141+
if ("hashedrekord".equals(body.getKind())) {
142+
var entry = RekorTypes.getHashedRekordV001(rekorEntry);
143+
switch (entry.getData().getHash().getAlgorithm()) {
144+
case SHA_256:
145+
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
146+
case SHA_384:
147+
return AlgorithmRegistry.HashAlgorithm.SHA2_384;
148+
case SHA_512:
149+
return AlgorithmRegistry.HashAlgorithm.SHA2_512;
150+
}
151+
} else if ("dsse".equals(body.getKind())) {
152+
// dsse:0.0.1 is always sha256
153+
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
147154
}
148-
} else if ("dsse".equals(body.getKind())) {
149-
// dsse:0.0.1 is always sha256
150-
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
151155
}
152-
} else if ("0.0.2".equals(body.getApiVersion())) {
153-
// rekor v2 entries must conform to the Algorithm Registry
154-
var publicKey = Certificates.getLeaf(bundle.getCertPath()).getPublicKey();
155-
var signingAlgorithm = AlgorithmRegistry.getSigningAlgorithm(publicKey);
156-
return signingAlgorithm.getHashAlgorithm();
157156
}
158-
throw new KeylessVerificationException(
159-
"Unsupported entry type: '" + body.getKind() + ":" + body.getApiVersion() + "'");
157+
var publicKey = Certificates.getLeaf(bundle.getCertPath()).getPublicKey();
158+
return AlgorithmRegistry.getSigningAlgorithm(publicKey).getHashAlgorithm();
160159
} catch (JsonParseException | RekorTypeException | UnsupportedAlgorithmException ex) {
161160
throw new KeylessVerificationException(
162161
"Could not determine hash algorithm from sigstore bundle", ex);
@@ -199,7 +198,7 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
199198

200199
// a trusted CT log
201200
try {
202-
fulcioVerifier.verifySigningCertificate(signingCert);
201+
fulcioVerifier.verifySigningCertificate(signingCert, options.getCtLogOptions());
203202
} catch (FulcioVerificationException | IOException ex) {
204203
throw new KeylessVerificationException(
205204
"Fulcio certificate was not valid: " + ex.getMessage(), ex);
@@ -209,7 +208,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
209208
checkCertificateMatchers(leafCert, options.getCertificateMatchers());
210209

211210
var hashAlgorithm = findHashAlgorithm(bundle);
212-
var rekorEntry = bundle.getEntries().get(0);
211+
212+
// A transparency-log entry is required unless disabled (e.g. a private deployment that verifies
213+
// via a signed timestamp instead). We also refuse to ignore an entry that is present, so a
214+
// disabled policy cannot silently skip verifying a log entry the bundle actually contains.
215+
var entries = bundle.getEntries();
216+
if (entries.isEmpty() && options.getTLogOptions().isEnabled()) {
217+
throw new KeylessVerificationException(
218+
"No transparency log entry found and transparency-log verification is required");
219+
}
220+
if (!entries.isEmpty() && !options.getTLogOptions().isEnabled()) {
221+
throw new KeylessVerificationException(
222+
"Bundle contains a transparency log entry but transparency-log verification is disabled");
223+
}
224+
var rekorEntry = entries.isEmpty() ? null : entries.get(0);
213225

214226
byte[] signature;
215227
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
@@ -222,16 +234,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
222234
signature = dsseEnvelope.getSignature();
223235
}
224236

225-
try {
226-
rekorVerifier.verifyEntry(rekorEntry);
227-
} catch (RekorVerificationException ex) {
228-
throw new KeylessVerificationException("Transparency log entry could not be verified", ex);
237+
// A transparency-log entry that is present is always verified; opting in only relaxes the
238+
// requirement that one exists, never the verification of one that is provided.
239+
Instant entryTime = null;
240+
if (rekorEntry != null) {
241+
try {
242+
rekorVerifier.verifyEntry(rekorEntry);
243+
} catch (RekorVerificationException ex) {
244+
throw new KeylessVerificationException("Transparency log entry could not be verified", ex);
245+
}
246+
// if entry was verified and has a SET, get time from it
247+
var set = rekorEntry.getVerification().getSignedEntryTimestamp();
248+
entryTime = set != null ? rekorEntry.getIntegratedTimeInstant() : null;
229249
}
230250

231-
// if entry was verified and has a SET, get time from it
232-
var set = rekorEntry.getVerification().getSignedEntryTimestamp();
233-
var entryTime = set != null ? rekorEntry.getIntegratedTimeInstant() : null;
234-
235251
verifyTimestamps(leafCert, bundle.getTimestamps(), entryTime, signature);
236252
}
237253

@@ -325,6 +341,12 @@ private void checkMessageSignature(
325341
"Signature could not be processed: " + ex.getMessage(), ex);
326342
}
327343

344+
// No transparency-log entry (allowed via VerificationOptions): the artifact digest and
345+
// signature have been verified above, and there is nothing to cross-check against.
346+
if (rekorEntry == null) {
347+
return;
348+
}
349+
328350
// recreate the log entry and check if it matches what was provided in the entry
329351
String version;
330352
try {
@@ -469,6 +491,12 @@ private void checkDsseEnvelope(
469491
throw new KeylessVerificationException("Signature could not be processed", se);
470492
}
471493

494+
// No transparency-log entry (allowed via VerificationOptions): the payload subject digest and
495+
// DSSE signature have been verified above, and there is nothing to cross-check against.
496+
if (rekorEntry == null) {
497+
return;
498+
}
499+
472500
RekorEntryBody entryBody;
473501
try {
474502
entryBody = rekorEntry.getBodyDecoded();

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.security.cert.X509Certificate;
2121
import java.util.List;
2222
import java.util.function.Predicate;
23+
import org.immutables.value.Value.Default;
2324
import org.immutables.value.Value.Immutable;
2425

2526
@Immutable(singleton = true)
@@ -28,6 +29,40 @@ public interface VerificationOptions {
2829
/** An allow list of certificate identities to match with. */
2930
List<CertificateMatcher> getCertificateMatchers();
3031

32+
/** Transparency-log (rekor) verification policy; defaults to requiring an entry. */
33+
@Default
34+
default TLogOptions getTLogOptions() {
35+
return TLogOptions.builder().isEnabled(true).build();
36+
}
37+
38+
/** Certificate-transparency (SCT) verification policy; defaults to requiring an SCT. */
39+
@Default
40+
default CTLogOptions getCtLogOptions() {
41+
return CTLogOptions.builder().isEnabled(true).build();
42+
}
43+
44+
/** Transparency-log verification options. */
45+
@Immutable
46+
interface TLogOptions {
47+
/** Whether a transparency-log entry is required and verified. */
48+
boolean isEnabled();
49+
50+
static ImmutableTLogOptions.Builder builder() {
51+
return ImmutableTLogOptions.builder();
52+
}
53+
}
54+
55+
/** Certificate-transparency verification options. */
56+
@Immutable
57+
interface CTLogOptions {
58+
/** Whether a certificate-transparency log entry (SCT) is required and verified. */
59+
boolean isEnabled();
60+
61+
static ImmutableCTLogOptions.Builder builder() {
62+
return ImmutableCTLogOptions.builder();
63+
}
64+
}
65+
3166
/**
3267
* An interface for allowing matching of certificates. Use {@link #fulcio()} to instantiate the
3368
* default {@link FulcioCertificateMatcher} implementation. Custom implementations may throw

sigstore-java/src/main/java/dev/sigstore/bundle/BundleReader.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ static Bundle readBundle(Reader jsonReader) throws BundleParseException {
5050

5151
bundleBuilder.mediaType(protoBundle.getMediaType());
5252

53-
if (protoBundle.getVerificationMaterial().getTlogEntriesCount() == 0) {
54-
throw new BundleParseException("Could not find any tlog entries in bundle json");
55-
}
5653
for (var bundleEntry : protoBundle.getVerificationMaterial().getTlogEntriesList()) {
5754
if (!bundleEntry.hasInclusionProof()) {
5855
// all consumed bundles must have an inclusion proof

sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package dev.sigstore.fulcio.client;
1717

1818
import com.google.common.annotations.VisibleForTesting;
19+
import dev.sigstore.VerificationOptions.CTLogOptions;
1920
import dev.sigstore.encryption.certificates.Certificates;
2021
import dev.sigstore.encryption.certificates.transparency.CTLogInfo;
2122
import dev.sigstore.encryption.certificates.transparency.CTVerificationResult;
@@ -44,7 +45,7 @@
4445
import java.util.Map;
4546
import java.util.stream.Collectors;
4647

47-
/** Verifier for fulcio generated signing cerificates */
48+
/** Verifier for fulcio generated signing certificates */
4849
public class FulcioVerifier {
4950
private final List<CertificateAuthority> cas;
5051
private final List<TransparencyLog> ctLogs;
@@ -94,8 +95,13 @@ private FulcioVerifier(
9495
}
9596

9697
@VisibleForTesting
97-
void verifySct(CertPath fullCertPath) throws FulcioVerificationException {
98-
if (ctLogs.size() == 0) {
98+
void verifySct(CertPath fullCertPath, CTLogOptions ctLogOptions)
99+
throws FulcioVerificationException {
100+
if (ctLogs.isEmpty()) {
101+
// Only when there are no CT Logs (private deployment) we can skip SCT verification
102+
if (!ctLogOptions.isEnabled()) {
103+
return;
104+
}
99105
throw new FulcioVerificationException("No ct logs were provided to verifier");
100106
}
101107

@@ -150,8 +156,17 @@ private void verifyEmbeddedScts(CertPath certPath) throws FulcioVerificationExce
150156
*/
151157
public void verifySigningCertificate(CertPath signingCertificate)
152158
throws FulcioVerificationException, IOException {
159+
verifySigningCertificate(signingCertificate, CTLogOptions.builder().isEnabled(true).build());
160+
}
161+
162+
/**
163+
* Verify a signing certificate, applying the given certificate-transparency policy (see {@link
164+
* #verifySct(CertPath, CTLogOptions)}).
165+
*/
166+
public void verifySigningCertificate(CertPath signingCertificate, CTLogOptions ctLogOptions)
167+
throws FulcioVerificationException, IOException {
153168
CertPath fullCertPath = validateCertPath(signingCertificate);
154-
verifySct(fullCertPath);
169+
verifySct(fullCertPath, ctLogOptions);
155170
}
156171

157172
public CertPath trimTrustedParent(CertPath signingCertificate)
@@ -177,7 +192,8 @@ CertPath validateCertPath(CertPath signingCertificate) throws FulcioVerification
177192
} catch (NoSuchAlgorithmException e) {
178193
//
179194
throw new RuntimeException(
180-
"No PKIX CertPathValidator, we probably shouldn't be here, but this seems to be a system library error not a program control flow issue",
195+
"No PKIX CertPathValidator, we probably shouldn't be here, but this seems to be a system"
196+
+ " library error not a program control flow issue",
181197
e);
182198
}
183199

@@ -197,7 +213,8 @@ CertPath validateCertPath(CertPath signingCertificate) throws FulcioVerification
197213
pkixParams = new PKIXParameters(Collections.singleton(ca.asTrustAnchor()));
198214
} catch (InvalidAlgorithmParameterException | CertificateException e) {
199215
throw new RuntimeException(
200-
"Can't create PKIX parameters for fulcioRoot. This should have been checked when generating a verifier instance",
216+
"Can't create PKIX parameters for fulcioRoot. This should have been checked when"
217+
+ " generating a verifier instance",
201218
e);
202219
}
203220
pkixParams.setRevocationEnabled(false);

0 commit comments

Comments
 (0)