Skip to content

Commit 9b9fb4c

Browse files
committed
Add queryable trustroot containers
Specificially for CertificateAuthorities and TransparencyLogs We need to be able to query the trustroot for CAs and logs to initialize our signers and based on the material we are signing. This will eventually allow us to init a client directly from a trustroot Signed-off-by: Appu Goundan <[email protected]>
1 parent 9bebb37 commit 9b9fb4c

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)