Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add accessors to trustroot #432

Merged
merged 1 commit into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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