Skip to content

Commit 768542d

Browse files
Close #1251: Allow no transparency-log entries
* Add support for verifying bundles without transparency-log entries, relying on signed RFC 3161 timestamps.
1 parent 5e523f3 commit 768542d

3 files changed

Lines changed: 82 additions & 9 deletions

File tree

sigstore-cli/src/main/java/dev/sigstore/cli/Verify.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ static class Policy {
113113
required = false)
114114
Path workingDirectory;
115115

116+
@Option(
117+
names = {"--allow-non-transparency-log"},
118+
description =
119+
"verify a bundle that has no transparency-log entry, relying on a signed RFC 3161 "
120+
+ "timestamp instead (reduced assurance; for instances that do not publish to a "
121+
+ "transparency log, e.g. GitHub's Sigstore instance for private repositories)",
122+
required = false,
123+
defaultValue = "false")
124+
Boolean allowNonTransparencyLog;
125+
116126
@Override
117127
public Integer call() throws Exception {
118128
byte[] digest;
@@ -142,6 +152,9 @@ public Integer call() throws Exception {
142152
.subjectAlternativeName(StringMatcher.string(policy.certificateSan))
143153
.build());
144154
}
155+
if (allowNonTransparencyLog) {
156+
verificationOptionsBuilder.allowNonTransparencyLogVerification(true);
157+
}
145158
var verificationOptions = verificationOptionsBuilder.build();
146159

147160
KeylessVerifier verifier;

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ public Builder trustedRootProvider(TrustedRootProvider trustedRootProvider) {
132132
private AlgorithmRegistry.HashAlgorithm findHashAlgorithm(Bundle bundle)
133133
throws KeylessVerificationException {
134134
try {
135+
// When the bundle has no transparency-log entry (e.g. a signed-timestamp-only bundle),
136+
// derive the hash algorithm without one: DSSE bundles use SHA-256 (as in the dsse:0.0.1
137+
// case below), otherwise fall back to the signing certificate's algorithm. Whether such a
138+
// bundle is permitted at all is enforced in verify(...) via
139+
// VerificationOptions#allowNonTransparencyLogVerification.
140+
if (bundle.getEntries().isEmpty()) {
141+
if (bundle.getDsseEnvelope().isPresent()) {
142+
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
143+
}
144+
var publicKey = Certificates.getLeaf(bundle.getCertPath()).getPublicKey();
145+
return AlgorithmRegistry.getSigningAlgorithm(publicKey).getHashAlgorithm();
146+
}
135147
var rekorEntry = bundle.getEntries().get(0);
136148
var body = rekorEntry.getBodyDecoded();
137149
if ("0.0.1".equals(body.getApiVersion())) {
@@ -209,7 +221,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
209221
checkCertificateMatchers(leafCert, options.getCertificateMatchers());
210222

211223
var hashAlgorithm = findHashAlgorithm(bundle);
212-
var rekorEntry = bundle.getEntries().get(0);
224+
225+
// A transparency-log entry is required by default. Bundles from instances that do not publish
226+
// to a log (e.g. GitHub's Sigstore instance for private repositories) carry only a signed
227+
// timestamp; verifying those requires opting in and relies on the RFC 3161 timestamp (verified
228+
// below) for trusted time.
229+
var entries = bundle.getEntries();
230+
if (entries.isEmpty() && !options.allowNonTransparencyLogVerification()) {
231+
throw new KeylessVerificationException(
232+
"Bundle does not contain a transparency log entry. If it is from an instance that does "
233+
+ "not publish to a transparency log, set "
234+
+ "VerificationOptions.allowNonTransparencyLogVerification(true) to verify using its "
235+
+ "signed timestamp instead.");
236+
}
237+
var rekorEntry = entries.isEmpty() ? null : entries.get(0);
213238

214239
byte[] signature;
215240
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
@@ -222,16 +247,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
222247
signature = dsseEnvelope.getSignature();
223248
}
224249

225-
try {
226-
rekorVerifier.verifyEntry(rekorEntry);
227-
} catch (RekorVerificationException ex) {
228-
throw new KeylessVerificationException("Transparency log entry could not be verified", ex);
250+
// A transparency-log entry that is present is always verified; opting in only relaxes the
251+
// requirement that one exists, never the verification of one that is provided.
252+
Instant entryTime = null;
253+
if (rekorEntry != null) {
254+
try {
255+
rekorVerifier.verifyEntry(rekorEntry);
256+
} catch (RekorVerificationException ex) {
257+
throw new KeylessVerificationException("Transparency log entry could not be verified", ex);
258+
}
259+
// if entry was verified and has a SET, get time from it
260+
var set = rekorEntry.getVerification().getSignedEntryTimestamp();
261+
entryTime = set != null ? rekorEntry.getIntegratedTimeInstant() : null;
229262
}
230263

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-
235264
verifyTimestamps(leafCert, bundle.getTimestamps(), entryTime, signature);
236265
}
237266

@@ -325,6 +354,12 @@ private void checkMessageSignature(
325354
"Signature could not be processed: " + ex.getMessage(), ex);
326355
}
327356

357+
// No transparency-log entry (allowed via VerificationOptions): the artifact digest and
358+
// signature have been verified above, and there is nothing to cross-check against.
359+
if (rekorEntry == null) {
360+
return;
361+
}
362+
328363
// recreate the log entry and check if it matches what was provided in the entry
329364
String version;
330365
try {
@@ -469,6 +504,12 @@ private void checkDsseEnvelope(
469504
throw new KeylessVerificationException("Signature could not be processed", se);
470505
}
471506

507+
// No transparency-log entry (allowed via VerificationOptions): the payload subject digest and
508+
// DSSE signature have been verified above, and there is nothing to cross-check against.
509+
if (rekorEntry == null) {
510+
return;
511+
}
512+
472513
RekorEntryBody entryBody;
473514
try {
474515
entryBody = rekorEntry.getBodyDecoded();

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

Lines changed: 19 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,24 @@ public interface VerificationOptions {
2829
/** An allow list of certificate identities to match with. */
2930
List<CertificateMatcher> getCertificateMatchers();
3031

32+
/**
33+
* Whether to allow verifying a bundle that contains no transparency-log entry, relying solely on
34+
* a signed RFC 3161 timestamp for trusted time. Defaults to {@code false}, matching the Sigstore
35+
* public-good instance where a transparency-log entry is always present and required.
36+
*
37+
* <p>Enabling this weakens the guarantees: without a transparency-log entry the signature is not
38+
* publicly monitorable/auditable. Only enable it for bundles from instances that intentionally do
39+
* not publish to a transparency log (for example, GitHub's Sigstore instance for private
40+
* repositories), and pair it with a pinned trusted root. A signed timestamp is still required.
41+
*
42+
* <p>This only relaxes the <em>requirement</em> that a transparency-log entry exists; a
43+
* transparency-log entry that <em>is</em> present in the bundle is still fully verified.
44+
*/
45+
@Default
46+
default boolean allowNonTransparencyLogVerification() {
47+
return false;
48+
}
49+
3150
/**
3251
* An interface for allowing matching of certificates. Use {@link #fulcio()} to instantiate the
3352
* default {@link FulcioCertificateMatcher} implementation. Custom implementations may throw

0 commit comments

Comments
 (0)