diff --git a/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java b/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java index 02ce0397..05ba6eef 100644 --- a/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java +++ b/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java @@ -26,10 +26,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; public class Certificates { + private static final String SCT_X509_OID = "1.3.6.1.4.1.11129.2.4.2"; + /** Convert a certificate to a PEM encoded certificate. */ public static String toPemString(Certificate cert) throws IOException { var certWriter = new StringWriter(); @@ -137,15 +140,69 @@ public static CertPath toCertPath(Certificate certificate) throws CertificateExc return cf.generateCertPath(Collections.singletonList(certificate)); } - /** Appends an X509Certificate to a {@link CertPath} as a leaf. */ - public static CertPath appendCertPath(CertPath root, Certificate certificate) + /** Appends a CertPath to another {@link CertPath} as children. */ + public static CertPath appendCertPath(CertPath parent, Certificate child) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); List certs = - ImmutableList.builder() - .add(certificate) - .addAll(root.getCertificates()) - .build(); + ImmutableList.builder().add(child).addAll(parent.getCertificates()).build(); return cf.generateCertPath(certs); } + + /** + * Trims a parent CertPath from a provided CertPath. This is intended to be used to trim trusted + * root and intermediates from a full CertPath to reveal just the untrusted parts which can be + * distributed as part of a signature tuple or bundle. + * + * @param certPath a certificate path to trim from + * @param parentPath the parent certPath to trim off the full certPath + * @return a trimmed path + * @throws IllegalArgumentException if the trimPath is not a parent of the certPath or if they are + * the same length + * @throws CertificateException if an error occurs during CertPath construction + */ + public static CertPath trimParent(CertPath certPath, CertPath parentPath) + throws CertificateException { + if (!containsParent(certPath, parentPath)) { + throw new IllegalArgumentException("trim path was not the parent of the provider chain"); + } + var certs = certPath.getCertificates(); + var parent = parentPath.getCertificates(); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return cf.generateCertPath(certs.subList(0, certs.size() - parent.size())); + } + + /** Check if a parent certpath is the suffix of a certpath */ + public static boolean containsParent(CertPath certPath, CertPath parentPath) { + var certs = certPath.getCertificates(); + var parent = parentPath.getCertificates(); + return parent.size() <= certs.size() + && certs.subList(certs.size() - parent.size(), certs.size()).equals(parent); + } + + /** + * Find and return any SCTs embedded in a certificate. + * + * @param certificate the certificate with embedded scts + * @return a byte array containing any number of embedded scts + */ + public static Optional getEmbeddedSCTs(Certificate certificate) { + return Optional.ofNullable(((X509Certificate) certificate).getExtensionValue(SCT_X509_OID)); + } + + /** Check if a certificate is self-signed. */ + public static boolean isSelfSigned(Certificate certificate) { + return ((X509Certificate) certificate) + .getIssuerX500Principal() + .equals(((X509Certificate) certificate).getSubjectX500Principal()); + } + + /** Check if the root of a CertPath is self-signed */ + public static boolean isSelfSigned(CertPath certPath) { + return isSelfSigned(certPath.getCertificates().get(certPath.getCertificates().size() - 1)); + } + + public static X509Certificate getLeaf(CertPath certPath) { + return (X509Certificate) certPath.getCertificates().get(0); + } } diff --git a/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java b/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java index efe70bb1..6d70d0b7 100644 --- a/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.List; import org.bouncycastle.util.encoders.Base64; @@ -134,16 +135,55 @@ public void toCertPath() throws Exception { @Test public void appendCertPath() throws Exception { - var certPath = + var parent = Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT_CHAIN))); - var cert = Certificates.fromPem(Resources.toByteArray(Resources.getResource(CERT_GH))); + var child = Certificates.fromPem(Resources.toByteArray(Resources.getResource(CERT_GH))); - Assertions.assertEquals(2, certPath.getCertificates().size()); - var appended = Certificates.appendCertPath(certPath, cert); + Assertions.assertEquals(2, parent.getCertificates().size()); + var appended = Certificates.appendCertPath(parent, child); Assertions.assertEquals(3, appended.getCertificates().size()); - Assertions.assertEquals(cert, appended.getCertificates().get(0)); - Assertions.assertEquals(certPath.getCertificates().get(0), appended.getCertificates().get(1)); - Assertions.assertEquals(certPath.getCertificates().get(1), appended.getCertificates().get(2)); + Assertions.assertEquals(child, appended.getCertificates().get(0)); + Assertions.assertEquals(parent.getCertificates().get(0), appended.getCertificates().get(1)); + Assertions.assertEquals(parent.getCertificates().get(1), appended.getCertificates().get(2)); + } + + @Test + public void trimParent() throws Exception { + var certPath = + Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT_CHAIN))); + var parent = + CertificateFactory.getInstance("X.509") + .generateCertPath(List.of(certPath.getCertificates().get(1))); + + var trimmed = Certificates.trimParent(certPath, parent); + + Assertions.assertEquals(1, trimmed.getCertificates().size()); + Assertions.assertEquals(certPath.getCertificates().get(0), trimmed.getCertificates().get(0)); + } + + @Test + public void containsParent() throws Exception { + var certPath = + Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT_CHAIN))); + var parent = + CertificateFactory.getInstance("X.509") + .generateCertPath(List.of(certPath.getCertificates().get(1))); + var cert = Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT))); + + Assertions.assertTrue(Certificates.containsParent(certPath, parent)); + Assertions.assertFalse(Certificates.containsParent(cert, certPath)); + Assertions.assertTrue(Certificates.containsParent(certPath, certPath)); + Assertions.assertTrue(Certificates.containsParent(cert, cert)); + } + + @Test + public void isSelfSigned() throws Exception { + var certPath = + Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT_CHAIN))); + var cert = Certificates.fromPem(Resources.toByteArray(Resources.getResource(CERT))); + + Assertions.assertTrue(Certificates.isSelfSigned(certPath)); + Assertions.assertFalse(Certificates.isSelfSigned(cert)); } }