Skip to content

Commit 08a3c5c

Browse files
authored
Merge pull request #487 from sigstore/tuf-in-rekor1
configure rekor signer (v2 for now) with trustroot
2 parents 326cf1d + 8c8f7f4 commit 08a3c5c

File tree

2 files changed

+377
-0
lines changed

2 files changed

+377
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2022 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 static dev.sigstore.json.GsonSupplier.GSON;
19+
20+
import com.google.api.client.http.ByteArrayContent;
21+
import com.google.api.client.http.GenericUrl;
22+
import com.google.api.client.http.HttpRequest;
23+
import com.google.api.client.http.HttpResponse;
24+
import com.google.api.client.http.HttpResponseException;
25+
import com.google.common.base.Preconditions;
26+
import dev.sigstore.http.HttpClients;
27+
import dev.sigstore.http.HttpParams;
28+
import dev.sigstore.http.ImmutableHttpParams;
29+
import dev.sigstore.trustroot.TransparencyLog;
30+
import java.io.IOException;
31+
import java.net.URI;
32+
import java.util.Arrays;
33+
import java.util.HashMap;
34+
import java.util.List;
35+
import java.util.Locale;
36+
import java.util.Optional;
37+
38+
/** A client to communicate with a rekor service instance. */
39+
public class RekorClient2 {
40+
public static final String REKOR_ENTRIES_PATH = "/api/v1/log/entries";
41+
public static final String REKOR_INDEX_SEARCH_PATH = "/api/v1/index/retrieve";
42+
43+
private final HttpParams httpParams;
44+
private final TransparencyLog tlog;
45+
46+
public static RekorClient2.Builder builder() {
47+
return new RekorClient2.Builder();
48+
}
49+
50+
private RekorClient2(HttpParams httpParams, TransparencyLog tlog) {
51+
this.tlog = tlog;
52+
this.httpParams = httpParams;
53+
}
54+
55+
public static class Builder {
56+
private HttpParams httpParams = ImmutableHttpParams.builder().build();
57+
private TransparencyLog tlog;
58+
59+
private Builder() {}
60+
61+
/** Configure the http properties, see {@link HttpParams}, {@link ImmutableHttpParams}. */
62+
public Builder setHttpParams(HttpParams httpParams) {
63+
this.httpParams = httpParams;
64+
return this;
65+
}
66+
67+
/** Configure the remote rekor instance to communicate with. */
68+
public Builder setTransparencyLog(TransparencyLog tlog) {
69+
this.tlog = tlog;
70+
return this;
71+
}
72+
73+
public RekorClient2 build() {
74+
Preconditions.checkNotNull(tlog);
75+
return new RekorClient2(httpParams, tlog);
76+
}
77+
}
78+
79+
/**
80+
* Put a new hashedrekord entry on the Rekor log.
81+
*
82+
* @param hashedRekordRequest the request to send to rekor
83+
* @return a {@link RekorResponse} with information about the log entry
84+
*/
85+
public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest)
86+
throws IOException, RekorParseException {
87+
URI rekorPutEndpoint = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH);
88+
89+
HttpRequest req =
90+
HttpClients.newRequestFactory(httpParams)
91+
.buildPostRequest(
92+
new GenericUrl(rekorPutEndpoint),
93+
ByteArrayContent.fromString(
94+
"application/json", hashedRekordRequest.toJsonPayload()));
95+
req.getHeaders().set("Accept", "application/json");
96+
req.getHeaders().set("Content-Type", "application/json");
97+
98+
HttpResponse resp = req.execute();
99+
if (resp.getStatusCode() != 201) {
100+
throw new IOException(
101+
String.format(
102+
Locale.ROOT,
103+
"bad response from rekor @ '%s' : %s",
104+
rekorPutEndpoint,
105+
resp.parseAsString()));
106+
}
107+
108+
URI rekorEntryUri = tlog.getBaseUrl().resolve(resp.getHeaders().getLocation());
109+
String entry = resp.parseAsString();
110+
return RekorResponse.newRekorResponse(rekorEntryUri, entry);
111+
}
112+
113+
public Optional<RekorEntry> getEntry(HashedRekordRequest hashedRekordRequest)
114+
throws IOException, RekorParseException {
115+
return getEntry(hashedRekordRequest.computeUUID());
116+
}
117+
118+
public Optional<RekorEntry> getEntry(String UUID) throws IOException, RekorParseException {
119+
URI getEntryURI = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH + "/" + UUID);
120+
HttpRequest req =
121+
HttpClients.newRequestFactory(httpParams).buildGetRequest(new GenericUrl(getEntryURI));
122+
req.getHeaders().set("Accept", "application/json");
123+
HttpResponse response;
124+
try {
125+
response = req.execute();
126+
} catch (HttpResponseException e) {
127+
if (e.getStatusCode() == 404) return Optional.empty();
128+
throw e;
129+
}
130+
return Optional.of(
131+
RekorResponse.newRekorResponse(getEntryURI, response.parseAsString()).getEntry());
132+
}
133+
134+
/**
135+
* Returns a list of UUIDs for matching entries for the given search parameters.
136+
*
137+
* @param email the OIDC email subject
138+
* @param hash sha256 hash of the artifact
139+
* @param publicKeyFormat format of public key (one of 'pgp','x509','minisign', 'ssh', 'tuf')
140+
* @param publicKeyContent public key base64 encoded content
141+
*/
142+
public List<String> searchEntry(
143+
String email, String hash, String publicKeyFormat, String publicKeyContent)
144+
throws IOException {
145+
URI rekorSearchEndpoint = tlog.getBaseUrl().resolve(REKOR_INDEX_SEARCH_PATH);
146+
147+
HashMap<String, Object> publicKeyParams = null;
148+
if (publicKeyContent != null) {
149+
publicKeyParams = new HashMap<>();
150+
publicKeyParams.put("format", publicKeyFormat);
151+
publicKeyParams.put("content", publicKeyContent);
152+
}
153+
var data = new HashMap<String, Object>();
154+
data.put("email", email);
155+
data.put("hash", hash);
156+
data.put("publicKey", publicKeyParams);
157+
158+
String contentString = GSON.get().toJson(data);
159+
HttpRequest req =
160+
HttpClients.newRequestFactory(httpParams)
161+
.buildPostRequest(
162+
new GenericUrl(rekorSearchEndpoint),
163+
ByteArrayContent.fromString("application/json", contentString));
164+
req.getHeaders().set("Accept", "application/json");
165+
req.getHeaders().set("Content-Type", "application/json");
166+
var response = req.execute();
167+
return Arrays.asList(GSON.get().fromJson(response.parseAsString(), String[].class));
168+
}
169+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright 2022 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 static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
22+
import com.google.common.collect.ImmutableList;
23+
import dev.sigstore.encryption.certificates.Certificates;
24+
import dev.sigstore.encryption.signers.Signers;
25+
import dev.sigstore.testing.CertGenerator;
26+
import dev.sigstore.trustroot.ImmutableTransparencyLog;
27+
import dev.sigstore.trustroot.LogId;
28+
import dev.sigstore.trustroot.PublicKey;
29+
import java.io.IOException;
30+
import java.net.URI;
31+
import java.net.URISyntaxException;
32+
import java.nio.charset.StandardCharsets;
33+
import java.security.InvalidKeyException;
34+
import java.security.MessageDigest;
35+
import java.security.NoSuchAlgorithmException;
36+
import java.security.SignatureException;
37+
import java.security.cert.CertificateException;
38+
import java.util.Optional;
39+
import java.util.UUID;
40+
import org.bouncycastle.operator.OperatorCreationException;
41+
import org.hamcrest.CoreMatchers;
42+
import org.hamcrest.MatcherAssert;
43+
import org.jetbrains.annotations.NotNull;
44+
import org.junit.jupiter.api.Assertions;
45+
import org.junit.jupiter.api.BeforeEach;
46+
import org.junit.jupiter.api.Test;
47+
import org.mockito.Mockito;
48+
49+
public class RekorClient2Test {
50+
51+
private static final String REKOR_URL = "https://rekor.sigstage.dev";
52+
private RekorClient2 client;
53+
54+
@BeforeEach
55+
public void setupClient() throws URISyntaxException {
56+
// this tests directly against rekor in staging, it's a bit hard to bring up a rekor instance
57+
// without docker compose.
58+
client =
59+
RekorClient2.builder()
60+
.setTransparencyLog(
61+
ImmutableTransparencyLog.builder()
62+
.baseUrl(URI.create(REKOR_URL))
63+
.hashAlgorithm("ignored")
64+
.publicKey(Mockito.mock(PublicKey.class))
65+
.logId(Mockito.mock(LogId.class))
66+
.build())
67+
.build();
68+
}
69+
70+
@Test
71+
public void putEntry_toStaging() throws Exception {
72+
HashedRekordRequest req = createdRekorRequest();
73+
var resp = client.putEntry(req);
74+
75+
// pretty basic testing
76+
MatcherAssert.assertThat(
77+
resp.getEntryLocation().toString(),
78+
CoreMatchers.startsWith(REKOR_URL + "/api/v1/log/entries/"));
79+
80+
assertNotNull(resp.getUuid());
81+
assertNotNull(resp.getRaw());
82+
var entry = resp.getEntry();
83+
assertNotNull(entry.getBody());
84+
Assertions.assertTrue(entry.getIntegratedTime() > 1);
85+
assertNotNull(entry.getLogID());
86+
Assertions.assertTrue(entry.getLogIndex() > 0);
87+
assertNotNull(entry.getVerification().getSignedEntryTimestamp());
88+
// Assertions.assertNotNull(entry.getVerification().getInclusionProof());
89+
}
90+
91+
// TODO([email protected]): don't use data from prod, create the data as part of the test
92+
// setup in staging.
93+
@Test
94+
public void searchEntries_nullParams() throws IOException {
95+
assertEquals(ImmutableList.of(), client.searchEntry(null, null, null, null));
96+
}
97+
98+
@Test
99+
public void searchEntries_oneResult_hash() throws Exception {
100+
var newRekordRequest = createdRekorRequest();
101+
client.putEntry(newRekordRequest);
102+
assertEquals(
103+
1,
104+
client
105+
.searchEntry(
106+
null, newRekordRequest.getHashedRekord().getData().getHash().getValue(), null, null)
107+
.size());
108+
}
109+
110+
@Test
111+
public void searchEntries_oneResult_publicKey() throws Exception {
112+
var newRekordRequest = createdRekorRequest();
113+
var resp = client.putEntry(newRekordRequest);
114+
assertEquals(
115+
1,
116+
client
117+
.searchEntry(
118+
null,
119+
null,
120+
"x509",
121+
RekorTypes.getHashedRekord(resp.getEntry())
122+
.getSignature()
123+
.getPublicKey()
124+
.getContent())
125+
.size());
126+
}
127+
128+
@Test
129+
public void searchEntries_moreThanOneResult_email() throws Exception {
130+
var newRekordRequest = createdRekorRequest();
131+
var newRekordRequest2 = createdRekorRequest();
132+
client.putEntry(newRekordRequest);
133+
client.putEntry(newRekordRequest2);
134+
assertTrue(
135+
client.searchEntry("[email protected]", null, null, null).size()
136+
> 1); // as long as our tests use staging this is just going to grow.
137+
}
138+
139+
@Test
140+
public void searchEntries_zeroResults() throws IOException {
141+
assertTrue(
142+
client
143+
.searchEntry(
144+
null,
145+
"sha256:9f54fad117567ab4c2c6738beef765f7c362550534ffc0bfe8d96b0236d69661", // made
146+
// up sha
147+
null,
148+
null)
149+
.isEmpty());
150+
}
151+
152+
@Test
153+
public void getEntry_entryExists() throws Exception {
154+
var newRekordRequest = createdRekorRequest();
155+
var resp = client.putEntry(newRekordRequest);
156+
var entry = client.getEntry(resp.getUuid());
157+
assertEntry(resp, entry);
158+
}
159+
160+
@Test
161+
public void getEntry_hashedRekordRequest_byCalculatedUuid() throws Exception {
162+
var hashedRekordRequest = createdRekorRequest();
163+
var resp = client.putEntry(hashedRekordRequest);
164+
// getting an entry by hashedrekordrequest should implicitly calculate uuid
165+
// from the contents of the hashedrekord
166+
var entry = client.getEntry(hashedRekordRequest);
167+
assertEntry(resp, entry);
168+
}
169+
170+
private void assertEntry(RekorResponse resp, Optional<RekorEntry> entry) {
171+
assertTrue(entry.isPresent());
172+
assertEquals(resp.getEntry().getLogID(), entry.get().getLogID());
173+
assertTrue(entry.get().getVerification().getInclusionProof().isPresent());
174+
assertNotNull(entry.get().getVerification().getInclusionProof().get().getTreeSize());
175+
assertNotNull(entry.get().getVerification().getInclusionProof().get().getRootHash());
176+
assertNotNull(entry.get().getVerification().getInclusionProof().get().getLogIndex());
177+
assertTrue(entry.get().getVerification().getInclusionProof().get().getHashes().size() > 0);
178+
}
179+
180+
@Test
181+
public void getEntry_entryDoesntExist() throws Exception {
182+
Optional<RekorEntry> entry =
183+
client.getEntry(
184+
"a8d2b213aa7efc1b2c9ccfa2fa647d00b34c63972e04e90276b5c31e0f317afd"); // I made this up
185+
assertTrue(entry.isEmpty());
186+
}
187+
188+
@NotNull
189+
private HashedRekordRequest createdRekorRequest()
190+
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
191+
OperatorCreationException, CertificateException, IOException {
192+
// the data we want to sign
193+
var data = "some data " + UUID.randomUUID();
194+
195+
// get the digest
196+
var artifactDigest =
197+
MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8));
198+
199+
// sign the full content (these signers do the artifact hashing themselves)
200+
var signer = Signers.newEcdsaSigner();
201+
var signature = signer.sign(data.getBytes(StandardCharsets.UTF_8));
202+
203+
// create a fake signing cert (not fulcio/dex)
204+
var cert = Certificates.toPemBytes(CertGenerator.newCert(signer.getPublicKey()));
205+
206+
return HashedRekordRequest.newHashedRekordRequest(artifactDigest, cert, signature);
207+
}
208+
}

0 commit comments

Comments
 (0)