Skip to content

Commit

Permalink
Add queryable trustroot containers
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
loosebazooka committed Aug 1, 2023
1 parent 9bebb37 commit 9b9fb4c
Show file tree
Hide file tree
Showing 9 changed files with 516 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.nio.charset.StandardCharsets;
import java.security.cert.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;

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

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

/** Convert a lit of der encoded certs to CertPath. */
public static CertPath fromDer(List<byte[]> certChain) throws CertificateException {
List<Certificate> certificates = new ArrayList<>(certChain.size());
for (var cert : certChain) {
certificates.add(fromDer(cert));
}
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return cf.generateCertPath(certificates);
}

/** Convert a CertPath to a PEM encoded certificate chain. */
public static String toPemString(CertPath certs) throws IOException {
var certWriter = new StringWriter();
Expand Down Expand Up @@ -116,4 +129,10 @@ public static CertPath fromPemChain(String certs) throws CertificateException {
public static CertPath fromPemChain(byte[] certs) throws CertificateException {
return fromPemChain(new String(certs, StandardCharsets.UTF_8));
}

/** Converts a single X509Certificate to a {@link CertPath}. */
public static CertPath toCertPath(Certificate certificate) throws CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return cf.generateCertPath(Collections.singletonList(certificate));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2023 The Sigstore Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sigstore.trustroot;

import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import org.immutables.value.Value;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;

@Immutable
@Value.Style(
depluralize = true,
depluralizeDictionary = {"certificateAuthority:certificateAuthorities"})
public abstract class CertificateAuthorities {

public abstract List<CertificateAuthority> getCertificateAuthorities();

@Derived
public int size() {
return getCertificateAuthorities().size();
}

@Derived
public List<CertificateAuthority> all() {
return getCertificateAuthorities();
}

/**
* Find a CA by validity time, users of this method will need to then compare the key in the leaf
* to find the exact CA to validate against
*
* @param time the time the CA was expected to be valid (usually tlog entry time)
* @return a list of CAs that were valid at {@code time}
*/
public List<CertificateAuthority> find(Instant time) {
return getCertificateAuthorities().stream()
.filter(ca -> ca.getValidFor().getStart().compareTo(time) <= 0)
.filter(ca -> ca.getValidFor().getEnd().orElse(Instant.now()).compareTo(time) >= 0)
.collect(Collectors.toList());
}

/**
* Get the one an only current Certificate Authority
*
* @return the current active CA
* @throws IllegalStateException if trust root does not contain exactly one active CA
*/
public CertificateAuthority current() {
var current =
getCertificateAuthorities().stream()
.filter(ca -> ca.getValidFor().getEnd().isEmpty())
.collect(Collectors.toList());
if (current.size() == 0) {
throw new IllegalStateException("Trust root contains no current certificate authorities");
}
if (current.size() > 1) {
throw new IllegalStateException(
"Trust root contains multiple current certificate authorities (" + current.size() + ")");
}
return current.get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,42 @@

import dev.sigstore.proto.trustroot.v1.TrustedRoot;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.immutables.value.Value.Immutable;

@Immutable
public interface SigstoreTrustedRoot {

List<CertificateAuthority> getCertificateAuthorities();
/** A list of certificate authorities associated with this trustroot. */
CertificateAuthorities getCAs();

List<TransparencyLog> getTLogs();
/** A list of binary transparency logs associated with this trustroot. */
TransparencyLogs getTLogs();

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

/** Create an instance from a parsed proto definition of a trustroot. */
static SigstoreTrustedRoot from(TrustedRoot proto) throws CertificateException {
List<CertificateAuthority> certificateAuthorities =
new ArrayList<>(proto.getCertificateAuthoritiesCount());
var certificateAuthoritiesBuilder = ImmutableCertificateAuthorities.builder();
for (var certAuthority : proto.getCertificateAuthoritiesList()) {
certificateAuthorities.add(CertificateAuthority.from(certAuthority));
certificateAuthoritiesBuilder.addCertificateAuthority(
CertificateAuthority.from(certAuthority));
}
var tlogs =
proto.getTlogsList().stream().map(TransparencyLog::from).collect(Collectors.toList());
var ctlogs =
proto.getCtlogsList().stream().map(TransparencyLog::from).collect(Collectors.toList());

var tlogsBuilder = ImmutableTransparencyLogs.builder();
proto.getTlogsList().stream()
.map(TransparencyLog::from)
.forEach(tlogsBuilder::addTransparencyLog);

var ctlogsBuilder = ImmutableTransparencyLogs.builder();
proto.getCtlogsList().stream()
.map(TransparencyLog::from)
.forEach(ctlogsBuilder::addTransparencyLog);

return ImmutableSigstoreTrustedRoot.builder()
.certificateAuthorities(certificateAuthorities)
.tLogs(tlogs)
.cTLogs(ctlogs)
.cAs(certificateAuthoritiesBuilder.build())
.tLogs(tlogsBuilder.build())
.cTLogs(ctlogsBuilder.build())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2023 The Sigstore Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sigstore.trustroot;

import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.immutables.value.Value;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;

@Immutable
@Value.Style(depluralize = true)
public abstract class TransparencyLogs {

public abstract List<TransparencyLog> getTransparencyLogs();

@Derived
public int size() {
return getTransparencyLogs().size();
}

@Derived
public List<TransparencyLog> all() {
return getTransparencyLogs();
}

public TransparencyLog current() {
var current =
getTransparencyLogs().stream()
.filter(tl -> tl.getPublicKey().getValidFor().getEnd().isEmpty())
.collect(Collectors.toList());
if (current.size() == 0) {
throw new IllegalStateException("Trust root contains no current transparency logs");
}
if (current.size() > 1) {
throw new IllegalStateException(
"Trust root contains multiple current transparency logs (" + current.size() + ")");
}
return current.get(0);
}

public Optional<TransparencyLog> find(byte[] logId, Instant time) {
return getTransparencyLogs().stream()
.filter(tl -> Arrays.equals(tl.getLogId().getKeyId(), logId))
.filter(tl -> tl.getPublicKey().getValidFor().getStart().compareTo(time) <= 0)
.filter(
tl ->
tl.getPublicKey().getValidFor().getEnd().orElse(Instant.now()).compareTo(time) >= 0)
.findAny();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.util.encoders.Base64;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -94,4 +97,37 @@ public void fromPemChain_garbage() throws IOException {
var pemString = "garbage";
Assertions.assertThrows(CertificateException.class, () -> Certificates.fromPemChain(pemString));
}

@Test
public void fromDer() throws Exception {
var derCert =
Base64.decode(
"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==");
Assertions.assertNotNull(Certificates.fromDer(derCert));
}

@Test
public void fromDer_certPath() throws Exception {
List<byte[]> certs = new ArrayList<>(2);
certs.add(
0,
Base64.decode(
"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="));
certs.add(
1,
Base64.decode(
"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"));
Assertions.assertEquals(2, Certificates.fromDer(certs).getCertificates().size());
}

@Test
public void toCertPath() throws Exception {
var cert =
Certificates.fromDer(
Base64.decode(
"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="));
var certPath = Certificates.toCertPath(cert);
Assertions.assertEquals(1, certPath.getCertificates().size());
Assertions.assertEquals(cert, certPath.getCertificates().get(0));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2023 The Sigstore Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sigstore.trustroot;

import static org.junit.jupiter.api.Assertions.*;

import java.net.URI;
import java.security.cert.CertPath;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class CertificateAuthoritiesTest {
@Test
public void current_missing() {
Assertions.assertThrows(
IllegalStateException.class,
() -> ImmutableCertificateAuthorities.builder().build().current());
}

@Test
public void current_tooMany() {
var ca =
ImmutableCertificateAuthority.builder()
.certPath(Mockito.mock(CertPath.class))
.uri(URI.create("abc"))
.subject(ImmutableSubject.builder().commonName("abc").organization("xyz").build())
.validFor(
ImmutableValidFor.builder()
.start(Instant.now().minus(10, ChronoUnit.SECONDS))
.build())
.build();
Assertions.assertThrows(
IllegalStateException.class,
() ->
ImmutableCertificateAuthorities.builder()
.addCertificateAuthorities(ca, ca)
.build()
.current());
}
}
Loading

0 comments on commit 9b9fb4c

Please sign in to comment.