Skip to content

Commit

Permalink
Merge pull request #432 from sigstore/plumb-tuf-client-part1
Browse files Browse the repository at this point in the history
Add accessors to trustroot
  • Loading branch information
loosebazooka authored Aug 3, 2023
2 parents eddc5a9 + 9b9fb4c commit 0d557a2
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 0d557a2

Please sign in to comment.