Skip to content

Commit 5bb4058

Browse files
committed
Add Rekor v2 client and verifier
Signed-off-by: Aaron Lew <64337293+aaronlew02@users.noreply.github.com>
1 parent 928045d commit 5bb4058

File tree

10 files changed

+810
-3
lines changed

10 files changed

+810
-3
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2025 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.encryption.signers;
17+
18+
import java.security.InvalidKeyException;
19+
import java.security.NoSuchAlgorithmException;
20+
import java.security.PublicKey;
21+
import java.security.Signature;
22+
import java.security.SignatureException;
23+
24+
/** Ed25519 verifier, instantiated by {@link Verifiers#newVerifier(PublicKey)}. */
25+
public class Ed25519Verifier implements Verifier {
26+
27+
private final PublicKey publicKey;
28+
29+
Ed25519Verifier(PublicKey publicKey) {
30+
this.publicKey = publicKey;
31+
}
32+
33+
@Override
34+
public PublicKey getPublicKey() {
35+
return publicKey;
36+
}
37+
38+
@Override
39+
public boolean verify(byte[] artifact, byte[] signature)
40+
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
41+
var verifier = Signature.getInstance("Ed25519");
42+
verifier.initVerify(publicKey);
43+
verifier.update(artifact);
44+
return verifier.verify(signature);
45+
}
46+
47+
@Override
48+
public boolean verifyDigest(byte[] digest, byte[] signature)
49+
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
50+
var verifier = Signature.getInstance("Ed25519");
51+
verifier.initVerify(publicKey);
52+
verifier.update(digest); // Treat artifactDigest as the message that was signed
53+
return verifier.verify(signature);
54+
}
55+
}

sigstore-java/src/main/java/dev/sigstore/encryption/signers/Verifiers.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@ public static Verifier newVerifier(PublicKey publicKey) throws NoSuchAlgorithmEx
2828
if (publicKey.getAlgorithm().equals("EC") || publicKey.getAlgorithm().equals("ECDSA")) {
2929
return new EcdsaVerifier(publicKey);
3030
}
31+
// EdDSA is the family, Ed25519 is a specific curve/algorithm instance.
32+
if (publicKey.getAlgorithm().equals("Ed25519")) {
33+
return new Ed25519Verifier(publicKey);
34+
}
3135
throw new NoSuchAlgorithmException(
3236
"Cannot verify signatures for key type '"
3337
+ publicKey.getAlgorithm()
34-
+ "', this client only supports RSA and ECDSA verification");
38+
+ "', this client only supports RSA, ECDSA, and Ed25519 verification");
3539
}
3640
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
* Checkpoint helper class to parse from a string in the format described in
3030
* https://github.com/transparency-dev/formats/blob/12bf59947efb7ae227c12f218b4740fb17a87e50/log/README.md
3131
*/
32-
class Checkpoints {
32+
public class Checkpoints {
3333
private static final Pattern SIGNATURE_BLOCK = Pattern.compile("\\u2014 (\\S+) (\\S+)");
3434

35-
static Checkpoint from(String encoded) throws RekorParseException {
35+
public static Checkpoint from(String encoded) throws RekorParseException {
3636
var split = Splitter.on("\n\n").splitToList(encoded);
3737
if (split.size() != 2) {
3838
throw new RekorParseException(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.rekor.v2.client;
17+
18+
import dev.sigstore.proto.rekor.v1.TransparencyLogEntry;
19+
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
20+
import dev.sigstore.rekor.client.RekorParseException;
21+
import java.io.IOException;
22+
23+
/** A client to communicate with a rekor v2 service instance. */
24+
public interface RekorV2Client {
25+
/**
26+
* Put a new hashedrekord entry on the Rekor log.
27+
*
28+
* @param hashedRekordRequest the request to send to rekor
29+
* @return a {@link TransparencyLogEntry} with information about the log entry
30+
*/
31+
TransparencyLogEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
32+
throws IOException, RekorParseException;
33+
34+
/**
35+
* Retrieve the latest checkpoint from the Rekor log.
36+
*
37+
* @return the checkpoint in plain text.
38+
*/
39+
String getCheckpoint() throws IOException;
40+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright 2025 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.rekor.v2.client;
17+
18+
import com.google.api.client.http.ByteArrayContent;
19+
import com.google.api.client.http.GenericUrl;
20+
import com.google.api.client.http.HttpRequest;
21+
import com.google.api.client.http.HttpResponse;
22+
import com.google.api.client.util.Preconditions;
23+
import com.google.gson.reflect.TypeToken;
24+
import com.google.protobuf.InvalidProtocolBufferException;
25+
import com.google.protobuf.util.JsonFormat;
26+
import dev.sigstore.http.HttpClients;
27+
import dev.sigstore.http.HttpParams;
28+
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;
32+
import dev.sigstore.proto.rekor.v2.HashedRekordRequestV002;
33+
import dev.sigstore.rekor.client.RekorParseException;
34+
import dev.sigstore.trustroot.Service;
35+
import java.io.IOException;
36+
import java.lang.reflect.Type;
37+
import java.net.URI;
38+
import java.util.Base64;
39+
import java.util.HashMap;
40+
import java.util.LinkedHashMap;
41+
import java.util.Locale;
42+
import java.util.Map;
43+
44+
/** A client to communicate with a rekor v2 service instance over http. */
45+
public class RekorV2ClientHttp implements RekorV2Client {
46+
public static final String REKOR_ENTRIES_PATH = "/api/v2/log/entries";
47+
public static final String REKOR_CHECKPOINT_PATH = "/api/v2/checkpoint";
48+
49+
private final HttpParams httpParams;
50+
private final URI uri;
51+
52+
public static RekorV2ClientHttp.Builder builder() {
53+
return new RekorV2ClientHttp.Builder();
54+
}
55+
56+
private RekorV2ClientHttp(HttpParams httpParams, URI uri) {
57+
this.uri = uri;
58+
this.httpParams = httpParams;
59+
}
60+
61+
public static class Builder {
62+
private HttpParams httpParams = ImmutableHttpParams.builder().build();
63+
private Service service;
64+
65+
private Builder() {}
66+
67+
/** Configure the http properties, see {@link HttpParams}, {@link ImmutableHttpParams}. */
68+
public Builder setHttpParams(HttpParams httpParams) {
69+
this.httpParams = httpParams;
70+
return this;
71+
}
72+
73+
/** Service information for a remote rekor instance. */
74+
public Builder setService(Service service) {
75+
this.service = service;
76+
return this;
77+
}
78+
79+
public RekorV2ClientHttp build() {
80+
Preconditions.checkNotNull(service);
81+
return new RekorV2ClientHttp(httpParams, service.getUrl());
82+
}
83+
}
84+
85+
@Override
86+
public TransparencyLogEntry putEntry(HashedRekordRequestV002 hashedRekordRequest)
87+
throws IOException, RekorParseException {
88+
URI rekorPutEndpoint = uri.resolve(REKOR_ENTRIES_PATH);
89+
90+
String jsonPayload;
91+
try {
92+
String innerJson = JsonFormat.printer().print(hashedRekordRequest);
93+
94+
Type type = new TypeToken<Map<String, Object>>() {}.getType();
95+
Map<String, Object> innerMap = GsonSupplier.GSON.get().fromJson(innerJson, type);
96+
97+
var requestMap = new HashMap<String, Object>();
98+
requestMap.put("hashedRekordRequestV002", innerMap);
99+
100+
jsonPayload = GsonSupplier.GSON.get().toJson(requestMap);
101+
} catch (InvalidProtocolBufferException e) {
102+
throw new RekorParseException("Failed to serialize HashedRekordRequestV002 to JSON", e);
103+
}
104+
105+
// jsonPayload = moreComplicatedJSONGeneration(hashedRekordRequest);
106+
107+
System.out.println(jsonPayload);
108+
109+
HttpRequest req =
110+
HttpClients.newRequestFactory(httpParams)
111+
.buildPostRequest(
112+
new GenericUrl(rekorPutEndpoint),
113+
ByteArrayContent.fromString("application/json", jsonPayload));
114+
req.getHeaders().set("Accept", "application/json");
115+
req.getHeaders().set("Content-Type", "application/json");
116+
117+
HttpResponse resp = req.execute();
118+
if (resp.getStatusCode() != 201) {
119+
throw new IOException(
120+
String.format(
121+
Locale.ROOT,
122+
"bad response from rekor @ '%s' : %s",
123+
rekorPutEndpoint,
124+
resp.parseAsString()));
125+
}
126+
127+
String respEntryJson = resp.parseAsString();
128+
129+
try {
130+
TransparencyLogEntry.Builder builder = TransparencyLogEntry.newBuilder();
131+
ProtoJson.parser().merge(respEntryJson, builder);
132+
return builder.build();
133+
} catch (InvalidProtocolBufferException e) {
134+
throw new RekorParseException("Failed to parse Rekor response JSON", e);
135+
}
136+
}
137+
138+
@Override
139+
public String getCheckpoint() throws IOException {
140+
URI rekorGetEndpoint = uri.resolve(REKOR_CHECKPOINT_PATH);
141+
142+
HttpRequest req =
143+
HttpClients.newRequestFactory(httpParams).buildGetRequest(new GenericUrl(rekorGetEndpoint));
144+
req.getHeaders().set("Accept", "text/plain");
145+
146+
HttpResponse resp = req.execute();
147+
148+
return resp.parseAsString();
149+
}
150+
151+
private String moreComplicatedJSONGeneration(HashedRekordRequestV002 hashedRekordRequest) {
152+
var data = new LinkedHashMap<String, Object>();
153+
var hashedRekorRequestData = new LinkedHashMap<String, Object>();
154+
var signatureData = new LinkedHashMap<String, Object>();
155+
156+
var digest = Base64.getEncoder().encodeToString(hashedRekordRequest.getDigest().toByteArray());
157+
158+
var signature = hashedRekordRequest.getSignature();
159+
var signatureContent = Base64.getEncoder().encodeToString(signature.getContent().toByteArray());
160+
161+
var verifier = signature.getVerifier();
162+
var verifierCertificate = verifier.getX509Certificate();
163+
var verifierCertificateRawBytes =
164+
Base64.getEncoder().encodeToString(verifierCertificate.getRawBytes().toByteArray());
165+
var verifierCertificateData = new LinkedHashMap<String, Object>();
166+
var verifierKeyDetails = verifier.getKeyDetails().toString();
167+
var signatureVerifier = new LinkedHashMap<String, Object>();
168+
169+
verifierCertificateData.put("rawBytes", verifierCertificateRawBytes);
170+
signatureVerifier.put("x509Certificate", verifierCertificateData);
171+
signatureVerifier.put("keyDetails", verifierKeyDetails);
172+
173+
signatureData.put("content", signatureContent);
174+
signatureData.put("verifier", signatureVerifier);
175+
176+
hashedRekorRequestData.put("digest", digest);
177+
hashedRekorRequestData.put("signature", signatureData);
178+
179+
data.put("hashedRekordRequestV002", hashedRekorRequestData);
180+
181+
return GsonSupplier.GSON.get().toJson(data);
182+
}
183+
}

0 commit comments

Comments
 (0)