@@ -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 ();
0 commit comments