@@ -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 ())) {
@@ -199,7 +211,8 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
199211
200212 // a trusted CT log
201213 try {
202- fulcioVerifier .verifySigningCertificate (signingCert );
214+ fulcioVerifier .verifySigningCertificate (
215+ signingCert , options .allowNonTransparencyLogVerification ());
203216 } catch (FulcioVerificationException | IOException ex ) {
204217 throw new KeylessVerificationException (
205218 "Fulcio certificate was not valid: " + ex .getMessage (), ex );
@@ -209,7 +222,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
209222 checkCertificateMatchers (leafCert , options .getCertificateMatchers ());
210223
211224 var hashAlgorithm = findHashAlgorithm (bundle );
212- var rekorEntry = bundle .getEntries ().get (0 );
225+
226+ // A transparency-log entry is required by default. Bundles from instances that do not publish
227+ // to a log (e.g. GitHub's Sigstore instance for private repositories) carry only a signed
228+ // timestamp; verifying those requires opting in and relies on the RFC 3161 timestamp (verified
229+ // below) for trusted time.
230+ var entries = bundle .getEntries ();
231+ if (entries .isEmpty () && !options .allowNonTransparencyLogVerification ()) {
232+ throw new KeylessVerificationException (
233+ "Bundle does not contain a transparency log entry. If it is from an instance that does "
234+ + "not publish to a transparency log, set "
235+ + "VerificationOptions.allowNonTransparencyLogVerification(true) to verify using its "
236+ + "signed timestamp instead." );
237+ }
238+ var rekorEntry = entries .isEmpty () ? null : entries .get (0 );
213239
214240 byte [] signature ;
215241 if (bundle .getMessageSignature ().isPresent ()) { // hashedrekord
@@ -222,16 +248,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
222248 signature = dsseEnvelope .getSignature ();
223249 }
224250
225- try {
226- rekorVerifier .verifyEntry (rekorEntry );
227- } catch (RekorVerificationException ex ) {
228- throw new KeylessVerificationException ("Transparency log entry could not be verified" , ex );
251+ // A transparency-log entry that is present is always verified; opting in only relaxes the
252+ // requirement that one exists, never the verification of one that is provided.
253+ Instant entryTime = null ;
254+ if (rekorEntry != null ) {
255+ try {
256+ rekorVerifier .verifyEntry (rekorEntry );
257+ } catch (RekorVerificationException ex ) {
258+ throw new KeylessVerificationException ("Transparency log entry could not be verified" , ex );
259+ }
260+ // if entry was verified and has a SET, get time from it
261+ var set = rekorEntry .getVerification ().getSignedEntryTimestamp ();
262+ entryTime = set != null ? rekorEntry .getIntegratedTimeInstant () : null ;
229263 }
230264
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-
235265 verifyTimestamps (leafCert , bundle .getTimestamps (), entryTime , signature );
236266 }
237267
@@ -325,6 +355,12 @@ private void checkMessageSignature(
325355 "Signature could not be processed: " + ex .getMessage (), ex );
326356 }
327357
358+ // No transparency-log entry (allowed via VerificationOptions): the artifact digest and
359+ // signature have been verified above, and there is nothing to cross-check against.
360+ if (rekorEntry == null ) {
361+ return ;
362+ }
363+
328364 // recreate the log entry and check if it matches what was provided in the entry
329365 String version ;
330366 try {
@@ -469,6 +505,12 @@ private void checkDsseEnvelope(
469505 throw new KeylessVerificationException ("Signature could not be processed" , se );
470506 }
471507
508+ // No transparency-log entry (allowed via VerificationOptions): the payload subject digest and
509+ // DSSE signature have been verified above, and there is nothing to cross-check against.
510+ if (rekorEntry == null ) {
511+ return ;
512+ }
513+
472514 RekorEntryBody entryBody ;
473515 try {
474516 entryBody = rekorEntry .getBodyDecoded ();
0 commit comments