Skip to content

Commit 767cc7b

Browse files
committed
Unify Rekor v1 and v2 verifiers
Signed-off-by: Aaron Lew <[email protected]>
1 parent 085e06f commit 767cc7b

File tree

9 files changed

+297
-568
lines changed

9 files changed

+297
-568
lines changed

sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import dev.sigstore.merkle.InclusionProofVerificationException;
2121
import dev.sigstore.merkle.InclusionProofVerifier;
2222
import dev.sigstore.rekor.client.RekorEntry.Checkpoint;
23+
import dev.sigstore.rekor.client.RekorEntry.CheckpointSignature;
2324
import dev.sigstore.trustroot.SigstoreTrustedRoot;
2425
import dev.sigstore.trustroot.TransparencyLog;
2526
import java.nio.charset.StandardCharsets;
@@ -31,6 +32,7 @@
3132
import java.util.Arrays;
3233
import java.util.Base64;
3334
import java.util.List;
35+
import java.util.Optional;
3436
import org.bouncycastle.util.encoders.Hex;
3537

3638
/** Verifier for rekor entries. */
@@ -50,42 +52,39 @@ private RekorVerifier(List<TransparencyLog> tlogs) {
5052
}
5153

5254
/**
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).
5457
*
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.
5761
*/
5862
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.");
6565
}
6666

6767
var tlog =
68-
TransparencyLog.find(tlogs, Hex.decode(entry.getLogID()), entry.getIntegratedTimeInstant())
68+
TransparencyLog.find(tlogs, Hex.decode(entry.getLogID()))
6969
.orElseThrow(
7070
() ->
7171
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);
8087
}
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.");
8988
}
9089

9190
// verify inclusion proof
@@ -126,34 +125,54 @@ private void verifyInclusionProof(RekorEntry entry) throws RekorVerificationExce
126125

127126
private void verifyCheckpoint(RekorEntry entry, TransparencyLog tlog)
128127
throws RekorVerificationException {
129-
Checkpoint checkpoint;
128+
Checkpoint parsedCheckpoint;
130129
try {
131-
checkpoint = entry.getVerification().getInclusionProof().parsedCheckpoint();
130+
parsedCheckpoint = entry.getVerification().getInclusionProof().parsedCheckpoint();
132131
} 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);
134142
}
135143

136144
byte[] inclusionRootHash =
137145
Hex.decode(entry.getVerification().getInclusionProof().getRootHash());
138-
byte[] checkpointRootHash = Base64.getDecoder().decode(checkpoint.getBase64Hash());
146+
byte[] checkpointRootHash = Base64.getDecoder().decode(parsedCheckpoint.getBase64Hash());
139147

140148
if (!Arrays.equals(inclusionRootHash, checkpointRootHash)) {
141149
throw new RekorVerificationException(
142150
"Checkpoint root hash does not match root hash provided in inclusion proof");
143151
}
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");
152169
}
153-
var signedData = checkpoint.getSignedData();
170+
171+
var signedData = parsedCheckpoint.getSignedData();
172+
154173
try {
155174
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())) {
157176
throw new RekorVerificationException("Checkpoint signature was invalid");
158177
}
159178
} catch (NoSuchAlgorithmException

sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2Client.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
*/
1616
package dev.sigstore.rekor.v2.client;
1717

18-
import dev.sigstore.proto.rekor.v1.TransparencyLogEntry;
1918
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
19+
import dev.sigstore.rekor.client.RekorEntry;
2020
import dev.sigstore.rekor.client.RekorParseException;
2121
import java.io.IOException;
2222

@@ -26,8 +26,8 @@ public interface RekorV2Client {
2626
* Put a new hashedrekord entry on the Rekor log.
2727
*
2828
* @param hashedRekordRequest the request to send to rekor
29-
* @return a {@link TransparencyLogEntry} with information about the log entry
29+
* @return a {@link RekorEntry} with information about the log entry
3030
*/
31-
TransparencyLogEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
31+
RekorEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
3232
throws IOException, RekorParseException;
3333
}

sigstore-java/src/main/java/dev/sigstore/rekor/v2/client/RekorV2ClientHttp.java

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,18 @@
2020
import com.google.api.client.http.HttpRequest;
2121
import com.google.api.client.http.HttpResponse;
2222
import com.google.api.client.util.Preconditions;
23-
import com.google.gson.reflect.TypeToken;
24-
import com.google.protobuf.InvalidProtocolBufferException;
2523
import com.google.protobuf.util.JsonFormat;
2624
import dev.sigstore.http.HttpClients;
2725
import dev.sigstore.http.HttpParams;
2826
import dev.sigstore.http.ImmutableHttpParams;
29-
import dev.sigstore.json.GsonSupplier;
30-
import dev.sigstore.json.ProtoJson;
31-
import dev.sigstore.proto.rekor.v1.TransparencyLogEntry;
27+
import dev.sigstore.proto.rekor.v2.CreateEntryRequest;
3228
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
29+
import dev.sigstore.rekor.client.RekorEntry;
3330
import dev.sigstore.rekor.client.RekorParseException;
3431
import dev.sigstore.trustroot.Service;
3532
import java.io.IOException;
36-
import java.lang.reflect.Type;
3733
import java.net.URI;
38-
import java.util.HashMap;
3934
import java.util.Locale;
40-
import java.util.Map;
4135

4236
/** A client to communicate with a rekor v2 service instance over http. */
4337
public class RekorV2ClientHttp implements RekorV2Client {
@@ -81,24 +75,16 @@ public RekorV2ClientHttp build() {
8175
}
8276

8377
@Override
84-
public TransparencyLogEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
78+
public RekorEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
8579
throws IOException, RekorParseException {
8680
URI rekorPutEndpoint = uri.resolve(REKOR_ENTRIES_PATH);
8781

88-
String jsonPayload;
89-
try {
90-
String innerJson = JsonFormat.printer().print(hashedRekordRequest);
91-
92-
Type type = new TypeToken<Map<String, Object>>() {}.getType();
93-
Map<String, Object> innerMap = GsonSupplier.GSON.get().fromJson(innerJson, type);
94-
95-
var requestMap = new HashMap<String, Object>();
96-
requestMap.put("hashedRekordRequestV002", innerMap);
97-
98-
jsonPayload = GsonSupplier.GSON.get().toJson(requestMap);
99-
} catch (InvalidProtocolBufferException e) {
100-
throw new RekorParseException("Failed to serialize HashedRekordRequestV002 to JSON", e);
101-
}
82+
String jsonPayload =
83+
JsonFormat.printer()
84+
.print(
85+
CreateEntryRequest.newBuilder()
86+
.setHashedRekordRequestV002(hashedRekordRequest)
87+
.build());
10288

10389
HttpRequest req =
10490
HttpClients.newRequestFactory(httpParams)
@@ -120,12 +106,6 @@ public TransparencyLogEntry putEntry(HashedRekordRequestV002 hashedRekordRequest
120106

121107
String respEntryJson = resp.parseAsString();
122108

123-
try {
124-
TransparencyLogEntry.Builder builder = TransparencyLogEntry.newBuilder();
125-
ProtoJson.parser().merge(respEntryJson, builder);
126-
return builder.build();
127-
} catch (InvalidProtocolBufferException e) {
128-
throw new RekorParseException("Failed to parse Rekor response JSON", e);
129-
}
109+
return RekorEntry.fromTLogEntryJson(respEntryJson);
130110
}
131111
}

0 commit comments

Comments
 (0)