Skip to content

Commit c984ae0

Browse files
committed
Allow RekorEntry to be built from TransparencyLogEntry
Signed-off-by: Aaron Lew <[email protected]>
1 parent 88eb9b4 commit c984ae0

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
import static dev.sigstore.json.GsonSupplier.GSON;
1919
import static java.nio.charset.StandardCharsets.UTF_8;
2020

21+
import com.google.protobuf.InvalidProtocolBufferException;
22+
import dev.sigstore.json.ProtoJson;
23+
import dev.sigstore.proto.rekor.v1.TransparencyLogEntry;
2124
import java.io.IOException;
2225
import java.time.Instant;
2326
import java.util.*;
2427
import javax.annotation.Nullable;
28+
import org.bouncycastle.util.encoders.Hex;
2529
import org.erdtman.jcs.JsonCanonicalizer;
2630
import org.immutables.gson.Gson;
2731
import org.immutables.value.Value;
@@ -41,6 +45,7 @@ interface Verification {
4145
String getSignedEntryTimestamp();
4246

4347
/** Return the inclusion proof. */
48+
@Nullable
4449
InclusionProof getInclusionProof();
4550
}
4651

@@ -162,4 +167,55 @@ default Instant getIntegratedTimeInstant() {
162167

163168
/** Returns the verification material for this entry. */
164169
Verification getVerification();
170+
171+
/** Returns a RekorEntry from the JSON representation of a TransparencyLogEntry */
172+
static RekorEntry fromTLogEntryJson(String json) throws RekorParseException {
173+
try {
174+
TransparencyLogEntry.Builder builder = TransparencyLogEntry.newBuilder();
175+
ProtoJson.parser().ignoringUnknownFields().merge(json, builder);
176+
return RekorEntry.fromTLogEntry(builder.build());
177+
} catch (InvalidProtocolBufferException e) {
178+
throw new RekorParseException("Failed to parse Rekor response JSON", e);
179+
}
180+
}
181+
182+
/** Returns a RekorEntry from a TransparencyLogEntry */
183+
public static RekorEntry fromTLogEntry(TransparencyLogEntry tle) throws RekorParseException {
184+
ImmutableRekorEntry.Builder builder = ImmutableRekorEntry.builder();
185+
186+
builder.logIndex(tle.getLogIndex());
187+
builder.logID(Hex.toHexString(tle.getLogId().getKeyId().toByteArray()));
188+
builder.integratedTime(tle.getIntegratedTime());
189+
190+
// The body of a RekorEntry is Base64 encoded
191+
builder.body(
192+
java.util.Base64.getEncoder().encodeToString(tle.getCanonicalizedBody().toByteArray()));
193+
194+
ImmutableVerification.Builder verificationBuilder = ImmutableVerification.builder();
195+
196+
// Rekor v2 entries won't have an InclusionPromise/SET
197+
if (tle.hasInclusionPromise()
198+
&& !tle.getInclusionPromise().getSignedEntryTimestamp().isEmpty()) {
199+
verificationBuilder.signedEntryTimestamp(
200+
java.util.Base64.getEncoder()
201+
.encodeToString(tle.getInclusionPromise().getSignedEntryTimestamp().toByteArray()));
202+
}
203+
204+
if (tle.hasInclusionProof()) {
205+
dev.sigstore.proto.rekor.v1.InclusionProof ipProto = tle.getInclusionProof();
206+
ImmutableInclusionProof.Builder ipBuilder = ImmutableInclusionProof.builder();
207+
ipBuilder.logIndex(ipProto.getLogIndex());
208+
ipBuilder.rootHash(
209+
org.bouncycastle.util.encoders.Hex.toHexString(ipProto.getRootHash().toByteArray()));
210+
ipBuilder.treeSize(ipProto.getTreeSize());
211+
ipBuilder.checkpoint(ipProto.getCheckpoint().getEnvelope());
212+
ipProto
213+
.getHashesList()
214+
.forEach(hash -> ipBuilder.addHashes(Hex.toHexString(hash.toByteArray())));
215+
verificationBuilder.inclusionProof(ipBuilder.build());
216+
}
217+
builder.verification(verificationBuilder.build());
218+
219+
return builder.build();
220+
}
165221
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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.client;
17+
18+
import com.google.protobuf.ByteString;
19+
import com.google.protobuf.util.JsonFormat;
20+
import dev.sigstore.proto.common.v1.LogId;
21+
import dev.sigstore.proto.rekor.v1.Checkpoint;
22+
import dev.sigstore.proto.rekor.v1.InclusionPromise;
23+
import dev.sigstore.proto.rekor.v1.InclusionProof;
24+
import dev.sigstore.proto.rekor.v1.KindVersion;
25+
import dev.sigstore.proto.rekor.v1.TransparencyLogEntry;
26+
import java.nio.charset.StandardCharsets;
27+
import java.util.Base64;
28+
import java.util.List;
29+
import org.erdtman.jcs.JsonCanonicalizer;
30+
import org.junit.jupiter.api.Assertions;
31+
import org.junit.jupiter.api.Test;
32+
33+
public class RekorEntryTest {
34+
35+
private static final String MOCK_BODY_JSON =
36+
"{\"apiVersion\":\"0.0.1\",\"kind\":\"hashedrekord\",\"spec\":{}}";
37+
private static final ByteString MOCK_BODY_BYTESTRING = ByteString.copyFromUtf8(MOCK_BODY_JSON);
38+
private static final String MOCK_BODY_B64 =
39+
Base64.getEncoder().encodeToString(MOCK_BODY_JSON.getBytes(StandardCharsets.UTF_8));
40+
41+
@Test
42+
public void fromTLogEntry_full() throws Exception {
43+
var tle =
44+
TransparencyLogEntry.newBuilder()
45+
.setLogIndex(123)
46+
.setLogId(LogId.newBuilder().setKeyId(ByteString.fromHex("abcdef")))
47+
.setIntegratedTime(456)
48+
.setKindVersion(KindVersion.newBuilder().setKind("hashedrekord").setVersion("0.0.1"))
49+
.setCanonicalizedBody(MOCK_BODY_BYTESTRING)
50+
.setInclusionPromise(
51+
InclusionPromise.newBuilder()
52+
.setSignedEntryTimestamp(ByteString.copyFromUtf8("set")))
53+
.setInclusionProof(
54+
InclusionProof.newBuilder()
55+
.setLogIndex(123)
56+
.setTreeSize(789)
57+
.setRootHash(ByteString.fromHex("fedcba"))
58+
.setCheckpoint(Checkpoint.newBuilder().setEnvelope("checkpoint envelope"))
59+
.addHashes(ByteString.fromHex("01"))
60+
.addHashes(ByteString.fromHex("02")))
61+
.build();
62+
63+
var entry = RekorEntry.fromTLogEntry(tle);
64+
65+
Assertions.assertEquals(123, entry.getLogIndex());
66+
Assertions.assertEquals("abcdef", entry.getLogID());
67+
Assertions.assertEquals(456, entry.getIntegratedTime());
68+
Assertions.assertEquals(MOCK_BODY_B64, entry.getBody());
69+
70+
var verification = entry.getVerification();
71+
Assertions.assertNotNull(verification);
72+
Assertions.assertEquals(
73+
Base64.getEncoder().encodeToString("set".getBytes(StandardCharsets.UTF_8)),
74+
verification.getSignedEntryTimestamp());
75+
76+
var inclusionProof = verification.getInclusionProof();
77+
Assertions.assertNotNull(inclusionProof);
78+
Assertions.assertEquals(123, inclusionProof.getLogIndex());
79+
Assertions.assertEquals(789, inclusionProof.getTreeSize());
80+
Assertions.assertEquals("fedcba", inclusionProof.getRootHash());
81+
Assertions.assertEquals("checkpoint envelope", inclusionProof.getCheckpoint());
82+
Assertions.assertEquals(List.of("01", "02"), inclusionProof.getHashes());
83+
}
84+
85+
@Test
86+
public void fromTLogEntry_minimal() throws Exception {
87+
// TLE with no inclusion promise or proof
88+
var tle =
89+
TransparencyLogEntry.newBuilder()
90+
.setLogIndex(123)
91+
.setLogId(LogId.newBuilder().setKeyId(ByteString.fromHex("abcdef")))
92+
.setIntegratedTime(456)
93+
.setKindVersion(KindVersion.newBuilder().setKind("hashedrekord").setVersion("0.0.1"))
94+
.setCanonicalizedBody(MOCK_BODY_BYTESTRING)
95+
.build();
96+
97+
var entry = RekorEntry.fromTLogEntry(tle);
98+
Assertions.assertEquals(123, entry.getLogIndex());
99+
Assertions.assertEquals("abcdef", entry.getLogID());
100+
Assertions.assertEquals(456, entry.getIntegratedTime());
101+
Assertions.assertEquals(MOCK_BODY_B64, entry.getBody());
102+
103+
var verification = entry.getVerification();
104+
Assertions.assertNotNull(verification);
105+
Assertions.assertNull(verification.getSignedEntryTimestamp());
106+
Assertions.assertNull(verification.getInclusionProof());
107+
}
108+
109+
@Test
110+
public void fromTLogEntryJson() throws Exception {
111+
var tle =
112+
TransparencyLogEntry.newBuilder()
113+
.setLogIndex(123)
114+
.setLogId(LogId.newBuilder().setKeyId(ByteString.fromHex("abcdef")))
115+
.setIntegratedTime(456)
116+
.setKindVersion(KindVersion.newBuilder().setKind("hashedrekord").setVersion("0.0.1"))
117+
.setCanonicalizedBody(MOCK_BODY_BYTESTRING)
118+
.build();
119+
120+
var json = JsonFormat.printer().print(tle);
121+
var entry = RekorEntry.fromTLogEntryJson(json);
122+
123+
Assertions.assertEquals(123, entry.getLogIndex());
124+
Assertions.assertEquals("abcdef", entry.getLogID());
125+
}
126+
127+
@Test
128+
public void fromTLogEntryJson_invalid() {
129+
var thrown =
130+
Assertions.assertThrows(
131+
RekorParseException.class, () -> RekorEntry.fromTLogEntryJson("{invalid"));
132+
Assertions.assertTrue(thrown.getMessage().startsWith("Failed to parse Rekor response JSON"));
133+
}
134+
135+
@Test
136+
public void getSignableContent() throws Exception {
137+
var entry =
138+
ImmutableRekorEntry.builder()
139+
.body(MOCK_BODY_B64)
140+
.integratedTime(456)
141+
.logID("abcdef")
142+
.logIndex(123)
143+
.verification(ImmutableVerification.builder().build())
144+
.build();
145+
146+
String expectedJson =
147+
"{\"body\":\""
148+
+ entry.getBody()
149+
+ "\",\"integratedTime\":456,\"logID\":\"abcdef\",\"logIndex\":123}";
150+
byte[] expectedCanonical = new JsonCanonicalizer(expectedJson).getEncodedUTF8();
151+
152+
Assertions.assertArrayEquals(expectedCanonical, entry.getSignableContent());
153+
}
154+
155+
@Test
156+
public void getBodyDecoded() throws Exception {
157+
var entry =
158+
ImmutableRekorEntry.builder()
159+
.body(MOCK_BODY_B64)
160+
.integratedTime(456)
161+
.logID("abcdef")
162+
.logIndex(123)
163+
.verification(ImmutableVerification.builder().build())
164+
.build();
165+
166+
var bodyDecoded = entry.getBodyDecoded();
167+
Assertions.assertEquals("0.0.1", bodyDecoded.getApiVersion());
168+
Assertions.assertEquals("hashedrekord", bodyDecoded.getKind());
169+
Assertions.assertNotNull(bodyDecoded.getSpec());
170+
}
171+
}

0 commit comments

Comments
 (0)