20
20
import dev .sigstore .merkle .InclusionProofVerificationException ;
21
21
import dev .sigstore .merkle .InclusionProofVerifier ;
22
22
import dev .sigstore .rekor .client .RekorEntry .Checkpoint ;
23
+ import dev .sigstore .rekor .client .RekorEntry .CheckpointSignature ;
23
24
import dev .sigstore .trustroot .SigstoreTrustedRoot ;
24
25
import dev .sigstore .trustroot .TransparencyLog ;
25
26
import java .nio .charset .StandardCharsets ;
31
32
import java .util .Arrays ;
32
33
import java .util .Base64 ;
33
34
import java .util .List ;
35
+ import java .util .Optional ;
34
36
import org .bouncycastle .util .encoders .Hex ;
35
37
36
38
/** Verifier for rekor entries. */
@@ -50,42 +52,39 @@ private RekorVerifier(List<TransparencyLog> tlogs) {
50
52
}
51
53
52
54
/**
53
- * Verify that a Rekor Entry is signed with the rekor public key loaded into this verifier
55
+ * Verifies a Rekor entry by checking the inclusion proof and checkpoint against the configured
56
+ * transparency logs. For v1 entries, it also verifies the Signed Entry Timestamp (SET).
54
57
*
55
- * @param entry the entry to verify
56
- * @throws RekorVerificationException if the entry cannot be verified
58
+ * @param entry The RekorEntry to verify.
59
+ * @throws RekorVerificationException if the entry cannot be verified for any reason, such as an
60
+ * invalid proof or signature.
57
61
*/
58
62
public void verifyEntry (RekorEntry entry ) throws RekorVerificationException {
59
- if (entry .getVerification () == null ) {
60
- throw new RekorVerificationException ("No verification information in entry." );
61
- }
62
-
63
- if (entry .getVerification ().getSignedEntryTimestamp () == null ) {
64
- throw new RekorVerificationException ("No signed entry timestamp found in entry." );
63
+ if (entry .getVerification ().getInclusionProof () == null ) {
64
+ throw new RekorVerificationException ("No inclusion proof in entry." );
65
65
}
66
66
67
67
var tlog =
68
- TransparencyLog .find (tlogs , Hex .decode (entry .getLogID ()), entry . getIntegratedTimeInstant () )
68
+ TransparencyLog .find (tlogs , Hex .decode (entry .getLogID ()))
69
69
.orElseThrow (
70
70
() ->
71
71
new RekorVerificationException (
72
- "Log entry (logid, timestamp) does not match any provided transparency logs." ));
73
-
74
- try {
75
- var verifier = Verifiers .newVerifier (tlog .getPublicKey ().toJavaPublicKey ());
76
- if (!verifier .verify (
77
- entry .getSignableContent (),
78
- Base64 .getDecoder ().decode (entry .getVerification ().getSignedEntryTimestamp ()))) {
79
- throw new RekorVerificationException ("Entry SET was not valid" );
72
+ "Log entry (logid) does not match any provided transparency logs." ));
73
+
74
+ if (entry .getVerification ().getSignedEntryTimestamp () != null ) {
75
+ try {
76
+ var verifier = Verifiers .newVerifier (tlog .getPublicKey ().toJavaPublicKey ());
77
+ if (!verifier .verify (
78
+ entry .getSignableContent (),
79
+ Base64 .getDecoder ().decode (entry .getVerification ().getSignedEntryTimestamp ()))) {
80
+ throw new RekorVerificationException ("Entry SET was not valid" );
81
+ }
82
+ } catch (InvalidKeySpecException
83
+ | InvalidKeyException
84
+ | SignatureException
85
+ | NoSuchAlgorithmException e ) {
86
+ throw new RekorVerificationException ("Entry SET verification failed: " + e .getMessage (), e );
80
87
}
81
- } catch (InvalidKeySpecException ike ) {
82
- throw new RekorVerificationException ("Public Key could be parsed" , ike );
83
- } catch (InvalidKeyException ike ) {
84
- throw new RekorVerificationException ("Public Key was invalid" , ike );
85
- } catch (SignatureException se ) {
86
- throw new RekorVerificationException ("Signature was invalid" , se );
87
- } catch (NoSuchAlgorithmException nsae ) {
88
- throw new AssertionError ("Required verification algorithm 'SHA256withECDSA' not found." );
89
88
}
90
89
91
90
// verify inclusion proof
@@ -126,34 +125,54 @@ private void verifyInclusionProof(RekorEntry entry) throws RekorVerificationExce
126
125
127
126
private void verifyCheckpoint (RekorEntry entry , TransparencyLog tlog )
128
127
throws RekorVerificationException {
129
- Checkpoint checkpoint ;
128
+ Checkpoint parsedCheckpoint ;
130
129
try {
131
- checkpoint = entry .getVerification ().getInclusionProof ().parsedCheckpoint ();
130
+ parsedCheckpoint = entry .getVerification ().getInclusionProof ().parsedCheckpoint ();
132
131
} catch (RekorParseException ex ) {
133
- throw new RekorVerificationException ("Could not parse checkpoint" , ex );
132
+ throw new RekorVerificationException ("Could not parse checkpoint from envelope" , ex );
133
+ }
134
+
135
+ final int MAX_CHECKPOINT_SIGNATURES = 20 ;
136
+ if (parsedCheckpoint .getSignatures ().size () > MAX_CHECKPOINT_SIGNATURES ) {
137
+ throw new RekorVerificationException (
138
+ "Checkpoint contains an excessive number of signatures ("
139
+ + parsedCheckpoint .getSignatures ().size ()
140
+ + "), exceeding the maximum allowed of "
141
+ + MAX_CHECKPOINT_SIGNATURES );
134
142
}
135
143
136
144
byte [] inclusionRootHash =
137
145
Hex .decode (entry .getVerification ().getInclusionProof ().getRootHash ());
138
- byte [] checkpointRootHash = Base64 .getDecoder ().decode (checkpoint .getBase64Hash ());
146
+ byte [] checkpointRootHash = Base64 .getDecoder ().decode (parsedCheckpoint .getBase64Hash ());
139
147
140
148
if (!Arrays .equals (inclusionRootHash , checkpointRootHash )) {
141
149
throw new RekorVerificationException (
142
150
"Checkpoint root hash does not match root hash provided in inclusion proof" );
143
151
}
144
- var keyHash = Hashing .sha256 ().hashBytes (tlog .getPublicKey ().getRawBytes ()).asBytes ();
145
- // checkpoint 0 is always the log, not any of the cross signing verifiers/monitors
146
- var sig = checkpoint .getSignatures ().get (0 );
147
- for (int i = 0 ; i < 4 ; i ++) {
148
- if (sig .getKeyHint ()[i ] != keyHash [i ]) {
149
- throw new RekorVerificationException (
150
- "Checkpoint key hint did not match provided log public key" );
151
- }
152
+
153
+ Optional <CheckpointSignature > matchingSig =
154
+ parsedCheckpoint .getSignatures ().stream ()
155
+ .filter (sig -> sig .getIdentity ().equals (tlog .getBaseUrl ().getHost ()))
156
+ .findFirst ();
157
+
158
+ if (!matchingSig .isPresent ()) {
159
+ throw new RekorVerificationException (
160
+ "No matching checkpoint signature found for transparency log: "
161
+ + tlog .getBaseUrl ().getHost ());
162
+ }
163
+
164
+ var keyId = tlog .getLogId ().getKeyId ();
165
+ var keyHint = Arrays .copyOfRange (keyId , 0 , 4 );
166
+ if (!Arrays .equals (matchingSig .get ().getKeyHint (), keyHint )) {
167
+ throw new RekorVerificationException (
168
+ "Checkpoint key hint did not match provided log public key" );
152
169
}
153
- var signedData = checkpoint .getSignedData ();
170
+
171
+ var signedData = parsedCheckpoint .getSignedData ();
172
+
154
173
try {
155
174
if (!Verifiers .newVerifier (tlog .getPublicKey ().toJavaPublicKey ())
156
- .verify (signedData .getBytes (StandardCharsets .UTF_8 ), sig .getSignature ())) {
175
+ .verify (signedData .getBytes (StandardCharsets .UTF_8 ), matchingSig . get () .getSignature ())) {
157
176
throw new RekorVerificationException ("Checkpoint signature was invalid" );
158
177
}
159
178
} catch (NoSuchAlgorithmException
0 commit comments