Skip to content

Commit

Permalink
Add accessors to trustroot
Browse files Browse the repository at this point in the history
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 Apr 25, 2023
1 parent 9bebb37 commit 6bb9a4d
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 8 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
Expand Up @@ -15,23 +15,31 @@
*/
package dev.sigstore.trustroot;

import com.google.common.annotations.VisibleForTesting;
import dev.sigstore.proto.trustroot.v1.TrustedRoot;
import java.security.cert.CertificateException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.immutables.value.Value.Immutable;

@Immutable
public interface SigstoreTrustedRoot {
public abstract class SigstoreTrustedRoot {

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

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

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

static SigstoreTrustedRoot from(TrustedRoot proto) throws CertificateException {
/** Create an instance from a parsed proto definition of a trustroot. */
public static SigstoreTrustedRoot from(TrustedRoot proto) throws CertificateException {
List<CertificateAuthority> certificateAuthorities =
new ArrayList<>(proto.getCertificateAuthoritiesCount());
for (var certAuthority : proto.getCertificateAuthoritiesList()) {
Expand All @@ -48,4 +56,114 @@ static SigstoreTrustedRoot from(TrustedRoot proto) throws CertificateException {
.cTLogs(ctlogs)
.build();
}

/**
* Find transparency log matching the logId and validity time.
*
* @param logId a log id
* @param time a point in time during the logs validity period
* @return the first log found matching logId and time, or empty if none match
*/
public Optional<TransparencyLog> getTlog(byte[] logId, Instant time) {
return getTLog(getTLogs(), logId, time);
}

/**
* Find certificate transparency log matching the logId and validity time.
*
* @param logId a log id
* @param time a point in time during the logs validity period
* @return the first log found matching logId and time, or empty if none match
*/
public Optional<TransparencyLog> getCTLog(byte[] logId, Instant time) {
return getTLog(getCTLogs(), logId, time);
}

private Optional<TransparencyLog> getTLog(
List<TransparencyLog> tlogs, byte[] logId, Instant time) {
return tlogs.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();
}

/**
* Get the one an only current TLog
*
* @return the current active TLog
* @throws IllegalStateException if trust root does not contain exactly one transparency log
*/
public Optional<TransparencyLog> getCurrentTLog() {
return getCurrentTLog(getTLogs());
}

/**
* Get the one an only current CTLog
*
* @return the current active CTLog
* @throws IllegalStateException if trust root does not contain exactly one active CT log
*/
public Optional<TransparencyLog> getCurrentCTLog() {
return getCurrentTLog(getCTLogs());
}

@VisibleForTesting
static Optional<TransparencyLog> getCurrentTLog(List<TransparencyLog> tlogs) {
var current =
tlogs.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 Optional.of(current.get(0));
}

/**
* 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> getCAs(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 Optional<CertificateAuthority> getCurrentCA() {
return getCurrentCA(getCertificateAuthorities());
}

@VisibleForTesting
static Optional<CertificateAuthority> getCurrentCA(
List<CertificateAuthority> certificateAuthorities) {
var current =
certificateAuthorities.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 Optional.of(current.get(0));
}
}
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));
}
}
Loading

0 comments on commit 6bb9a4d

Please sign in to comment.