Skip to content

Commit 0d557a2

Browse files
authored
Merge pull request #432 from sigstore/plumb-tuf-client-part1
Add accessors to trustroot
2 parents eddc5a9 + 9b9fb4c commit 0d557a2

File tree

9 files changed

+516
-25
lines changed

9 files changed

+516
-25
lines changed

sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.nio.charset.StandardCharsets;
2424
import java.security.cert.*;
2525
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
2628
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
2729

2830
public class Certificates {
@@ -55,11 +57,22 @@ public static Certificate fromPem(byte[] cert) throws CertificateException {
5557
return fromPem(new String(cert, StandardCharsets.UTF_8));
5658
}
5759

60+
/** Convert a single der encoded cert to Certificate. */
5861
public static Certificate fromDer(byte[] cert) throws CertificateException {
5962
CertificateFactory cf = CertificateFactory.getInstance("X.509");
6063
return cf.generateCertificate(new ByteArrayInputStream(cert));
6164
}
6265

66+
/** Convert a lit of der encoded certs to CertPath. */
67+
public static CertPath fromDer(List<byte[]> certChain) throws CertificateException {
68+
List<Certificate> certificates = new ArrayList<>(certChain.size());
69+
for (var cert : certChain) {
70+
certificates.add(fromDer(cert));
71+
}
72+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
73+
return cf.generateCertPath(certificates);
74+
}
75+
6376
/** Convert a CertPath to a PEM encoded certificate chain. */
6477
public static String toPemString(CertPath certs) throws IOException {
6578
var certWriter = new StringWriter();
@@ -116,4 +129,10 @@ public static CertPath fromPemChain(String certs) throws CertificateException {
116129
public static CertPath fromPemChain(byte[] certs) throws CertificateException {
117130
return fromPemChain(new String(certs, StandardCharsets.UTF_8));
118131
}
132+
133+
/** Converts a single X509Certificate to a {@link CertPath}. */
134+
public static CertPath toCertPath(Certificate certificate) throws CertificateException {
135+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
136+
return cf.generateCertPath(Collections.singletonList(certificate));
137+
}
119138
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2023 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.trustroot;
17+
18+
import java.time.Instant;
19+
import java.util.List;
20+
import java.util.stream.Collectors;
21+
import org.immutables.value.Value;
22+
import org.immutables.value.Value.Derived;
23+
import org.immutables.value.Value.Immutable;
24+
25+
@Immutable
26+
@Value.Style(
27+
depluralize = true,
28+
depluralizeDictionary = {"certificateAuthority:certificateAuthorities"})
29+
public abstract class CertificateAuthorities {
30+
31+
public abstract List<CertificateAuthority> getCertificateAuthorities();
32+
33+
@Derived
34+
public int size() {
35+
return getCertificateAuthorities().size();
36+
}
37+
38+
@Derived
39+
public List<CertificateAuthority> all() {
40+
return getCertificateAuthorities();
41+
}
42+
43+
/**
44+
* Find a CA by validity time, users of this method will need to then compare the key in the leaf
45+
* to find the exact CA to validate against
46+
*
47+
* @param time the time the CA was expected to be valid (usually tlog entry time)
48+
* @return a list of CAs that were valid at {@code time}
49+
*/
50+
public List<CertificateAuthority> find(Instant time) {
51+
return getCertificateAuthorities().stream()
52+
.filter(ca -> ca.getValidFor().getStart().compareTo(time) <= 0)
53+
.filter(ca -> ca.getValidFor().getEnd().orElse(Instant.now()).compareTo(time) >= 0)
54+
.collect(Collectors.toList());
55+
}
56+
57+
/**
58+
* Get the one an only current Certificate Authority
59+
*
60+
* @return the current active CA
61+
* @throws IllegalStateException if trust root does not contain exactly one active CA
62+
*/
63+
public CertificateAuthority current() {
64+
var current =
65+
getCertificateAuthorities().stream()
66+
.filter(ca -> ca.getValidFor().getEnd().isEmpty())
67+
.collect(Collectors.toList());
68+
if (current.size() == 0) {
69+
throw new IllegalStateException("Trust root contains no current certificate authorities");
70+
}
71+
if (current.size() > 1) {
72+
throw new IllegalStateException(
73+
"Trust root contains multiple current certificate authorities (" + current.size() + ")");
74+
}
75+
return current.get(0);
76+
}
77+
}

sigstore-java/src/main/java/dev/sigstore/trustroot/SigstoreTrustedRoot.java

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,42 @@
1717

1818
import dev.sigstore.proto.trustroot.v1.TrustedRoot;
1919
import java.security.cert.CertificateException;
20-
import java.util.ArrayList;
21-
import java.util.List;
22-
import java.util.stream.Collectors;
2320
import org.immutables.value.Value.Immutable;
2421

2522
@Immutable
2623
public interface SigstoreTrustedRoot {
2724

28-
List<CertificateAuthority> getCertificateAuthorities();
25+
/** A list of certificate authorities associated with this trustroot. */
26+
CertificateAuthorities getCAs();
2927

30-
List<TransparencyLog> getTLogs();
28+
/** A list of binary transparency logs associated with this trustroot. */
29+
TransparencyLogs getTLogs();
3130

32-
List<TransparencyLog> getCTLogs();
31+
/** A list of certificate transparency logs associated with this trustroot. */
32+
TransparencyLogs getCTLogs();
3333

34+
/** Create an instance from a parsed proto definition of a trustroot. */
3435
static SigstoreTrustedRoot from(TrustedRoot proto) throws CertificateException {
35-
List<CertificateAuthority> certificateAuthorities =
36-
new ArrayList<>(proto.getCertificateAuthoritiesCount());
36+
var certificateAuthoritiesBuilder = ImmutableCertificateAuthorities.builder();
3737
for (var certAuthority : proto.getCertificateAuthoritiesList()) {
38-
certificateAuthorities.add(CertificateAuthority.from(certAuthority));
38+
certificateAuthoritiesBuilder.addCertificateAuthority(
39+
CertificateAuthority.from(certAuthority));
3940
}
40-
var tlogs =
41-
proto.getTlogsList().stream().map(TransparencyLog::from).collect(Collectors.toList());
42-
var ctlogs =
43-
proto.getCtlogsList().stream().map(TransparencyLog::from).collect(Collectors.toList());
41+
42+
var tlogsBuilder = ImmutableTransparencyLogs.builder();
43+
proto.getTlogsList().stream()
44+
.map(TransparencyLog::from)
45+
.forEach(tlogsBuilder::addTransparencyLog);
46+
47+
var ctlogsBuilder = ImmutableTransparencyLogs.builder();
48+
proto.getCtlogsList().stream()
49+
.map(TransparencyLog::from)
50+
.forEach(ctlogsBuilder::addTransparencyLog);
4451

4552
return ImmutableSigstoreTrustedRoot.builder()
46-
.certificateAuthorities(certificateAuthorities)
47-
.tLogs(tlogs)
48-
.cTLogs(ctlogs)
53+
.cAs(certificateAuthoritiesBuilder.build())
54+
.tLogs(tlogsBuilder.build())
55+
.cTLogs(ctlogsBuilder.build())
4956
.build();
5057
}
5158
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2023 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.trustroot;
17+
18+
import java.time.Instant;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Optional;
22+
import java.util.stream.Collectors;
23+
import org.immutables.value.Value;
24+
import org.immutables.value.Value.Derived;
25+
import org.immutables.value.Value.Immutable;
26+
27+
@Immutable
28+
@Value.Style(depluralize = true)
29+
public abstract class TransparencyLogs {
30+
31+
public abstract List<TransparencyLog> getTransparencyLogs();
32+
33+
@Derived
34+
public int size() {
35+
return getTransparencyLogs().size();
36+
}
37+
38+
@Derived
39+
public List<TransparencyLog> all() {
40+
return getTransparencyLogs();
41+
}
42+
43+
public TransparencyLog current() {
44+
var current =
45+
getTransparencyLogs().stream()
46+
.filter(tl -> tl.getPublicKey().getValidFor().getEnd().isEmpty())
47+
.collect(Collectors.toList());
48+
if (current.size() == 0) {
49+
throw new IllegalStateException("Trust root contains no current transparency logs");
50+
}
51+
if (current.size() > 1) {
52+
throw new IllegalStateException(
53+
"Trust root contains multiple current transparency logs (" + current.size() + ")");
54+
}
55+
return current.get(0);
56+
}
57+
58+
public Optional<TransparencyLog> find(byte[] logId, Instant time) {
59+
return getTransparencyLogs().stream()
60+
.filter(tl -> Arrays.equals(tl.getLogId().getKeyId(), logId))
61+
.filter(tl -> tl.getPublicKey().getValidFor().getStart().compareTo(time) <= 0)
62+
.filter(
63+
tl ->
64+
tl.getPublicKey().getValidFor().getEnd().orElse(Instant.now()).compareTo(time) >= 0)
65+
.findAny();
66+
}
67+
}

sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import java.io.IOException;
2020
import java.nio.charset.StandardCharsets;
2121
import java.security.cert.CertificateException;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import org.bouncycastle.util.encoders.Base64;
2225
import org.junit.jupiter.api.Assertions;
2326
import org.junit.jupiter.api.Test;
2427

@@ -94,4 +97,37 @@ public void fromPemChain_garbage() throws IOException {
9497
var pemString = "garbage";
9598
Assertions.assertThrows(CertificateException.class, () -> Certificates.fromPemChain(pemString));
9699
}
100+
101+
@Test
102+
public void fromDer() throws Exception {
103+
var derCert =
104+
Base64.decode(
105+
"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==");
106+
Assertions.assertNotNull(Certificates.fromDer(derCert));
107+
}
108+
109+
@Test
110+
public void fromDer_certPath() throws Exception {
111+
List<byte[]> certs = new ArrayList<>(2);
112+
certs.add(
113+
0,
114+
Base64.decode(
115+
"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="));
116+
certs.add(
117+
1,
118+
Base64.decode(
119+
"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"));
120+
Assertions.assertEquals(2, Certificates.fromDer(certs).getCertificates().size());
121+
}
122+
123+
@Test
124+
public void toCertPath() throws Exception {
125+
var cert =
126+
Certificates.fromDer(
127+
Base64.decode(
128+
"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="));
129+
var certPath = Certificates.toCertPath(cert);
130+
Assertions.assertEquals(1, certPath.getCertificates().size());
131+
Assertions.assertEquals(cert, certPath.getCertificates().get(0));
132+
}
97133
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2023 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.trustroot;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
20+
import java.net.URI;
21+
import java.security.cert.CertPath;
22+
import java.time.Instant;
23+
import java.time.temporal.ChronoUnit;
24+
import org.junit.jupiter.api.Assertions;
25+
import org.junit.jupiter.api.Test;
26+
import org.mockito.Mockito;
27+
28+
class CertificateAuthoritiesTest {
29+
@Test
30+
public void current_missing() {
31+
Assertions.assertThrows(
32+
IllegalStateException.class,
33+
() -> ImmutableCertificateAuthorities.builder().build().current());
34+
}
35+
36+
@Test
37+
public void current_tooMany() {
38+
var ca =
39+
ImmutableCertificateAuthority.builder()
40+
.certPath(Mockito.mock(CertPath.class))
41+
.uri(URI.create("abc"))
42+
.subject(ImmutableSubject.builder().commonName("abc").organization("xyz").build())
43+
.validFor(
44+
ImmutableValidFor.builder()
45+
.start(Instant.now().minus(10, ChronoUnit.SECONDS))
46+
.build())
47+
.build();
48+
Assertions.assertThrows(
49+
IllegalStateException.class,
50+
() ->
51+
ImmutableCertificateAuthorities.builder()
52+
.addCertificateAuthorities(ca, ca)
53+
.build()
54+
.current());
55+
}
56+
}

0 commit comments

Comments
 (0)