From 0a3be1ee3a9254ffd6e15749ebd6d23a7b210f9a Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Fri, 8 Sep 2023 10:02:23 -0400 Subject: [PATCH] WIP Signed-off-by: Appu Goundan --- fuzzing/build.gradle.kts | 1 + .../java/fuzzing/FulcioVerifierFuzzer.java | 12 +- .../java/fuzzing/RekorVerifierFuzzer.java | 14 +- fuzzing/src/main/java/util/Tuf.java | 93 +++++ .../dev/sigstore/sign/work/SignWorkAction.kt | 4 +- .../main/java/dev/sigstore/KeylessSigner.java | 107 ++--- .../java/dev/sigstore/KeylessSigner2.java | 378 ------------------ .../java/dev/sigstore/KeylessVerifier.java | 69 ++-- .../java/dev/sigstore/KeylessVerifier2.java | 271 ------------- .../sigstore/fulcio/client/FulcioClient.java | 75 ++-- .../sigstore/fulcio/client/FulcioClient2.java | 134 ------- .../fulcio/client/FulcioVerifier.java | 251 +++++++----- .../fulcio/client/FulcioVerifier2.java | 215 ---------- .../sigstore/rekor/client/RekorClient.java | 50 ++- .../sigstore/rekor/client/RekorClient2.java | 176 -------- .../sigstore/rekor/client/RekorVerifier.java | 44 +- .../sigstore/rekor/client/RekorVerifier2.java | 160 -------- .../test/java/dev/sigstore/Keyless2Test.java | 8 +- .../fulcio/client/FulcioClient2Test.java | 102 ----- .../fulcio/client/FulcioClientTest.java | 35 +- .../fulcio/client/FulcioVerifier2Test.java | 167 -------- .../fulcio/client/FulcioVerifierTest.java | 105 +++-- .../rekor/client/RekorClient2Test.java | 208 ---------- .../rekor/client/RekorClientTest.java | 19 +- .../rekor/client/RekorVerifier2Test.java | 207 ---------- .../rekor/client/RekorVerifierTest.java | 111 ++++- .../trustroot/CertificateAuthoritiesTest.java | 2 - 27 files changed, 614 insertions(+), 2404 deletions(-) create mode 100644 fuzzing/src/main/java/util/Tuf.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient2.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier2.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient2.java delete mode 100644 sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier2.java delete mode 100644 sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClient2Test.java delete mode 100644 sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifier2Test.java delete mode 100644 sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClient2Test.java delete mode 100644 sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifier2Test.java diff --git a/fuzzing/build.gradle.kts b/fuzzing/build.gradle.kts index 0bbe76941..4d1e0ca0b 100644 --- a/fuzzing/build.gradle.kts +++ b/fuzzing/build.gradle.kts @@ -8,6 +8,7 @@ repositories { dependencies { implementation(project(":sigstore-java")) + implementation("com.google.guava:guava:31.1-jre") implementation("com.code-intelligence:jazzer-api:0.16.1") } diff --git a/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java b/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java index 1d657197f..6ec484d24 100644 --- a/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java +++ b/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java @@ -29,24 +29,24 @@ import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.List; +import util.Tuf; public class FulcioVerifierFuzzer { public static void fuzzerTestOneInput(FuzzedDataProvider data) { try { int[] intArray = data.consumeInts(data.consumeInt(1, 10)); - byte[] byteArray = data.consumeRemainingAsBytes(); - List certList = new ArrayList(); - List byteArrayList = new ArrayList(); + var cas = Tuf.certificateAuthoritiesFrom(data); + var ctLogs = Tuf.transparencyLogsFrom(data); + byte[] byteArray = data.consumeRemainingAsBytes(); + List certList = new ArrayList(); CertificateFactory cf = CertificateFactory.getInstance("X.509"); certList.add(cf.generateCertificate(new ByteArrayInputStream(byteArray))); certList.add(cf.generateCertificate(new ByteArrayInputStream(byteArray))); - byteArrayList.add(byteArray); - byteArrayList.add(byteArray); SigningCertificate sc = SigningCertificate.from(cf.generateCertPath(certList)); - FulcioVerifier fv = FulcioVerifier.newFulcioVerifier(byteArray, byteArrayList); + FulcioVerifier fv = FulcioVerifier.newFulcioVerifier(cas, ctLogs); for (int choice : intArray) { switch (choice % 4) { diff --git a/fuzzing/src/main/java/fuzzing/RekorVerifierFuzzer.java b/fuzzing/src/main/java/fuzzing/RekorVerifierFuzzer.java index 441514a77..8f42f0ea6 100644 --- a/fuzzing/src/main/java/fuzzing/RekorVerifierFuzzer.java +++ b/fuzzing/src/main/java/fuzzing/RekorVerifierFuzzer.java @@ -21,33 +21,27 @@ import dev.sigstore.rekor.client.RekorResponse; import dev.sigstore.rekor.client.RekorVerificationException; import dev.sigstore.rekor.client.RekorVerifier; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; +import util.Tuf; public class RekorVerifierFuzzer { private static final String URL = "https://false.url.for.RekorTypes.fuzzing.com"; public static void fuzzerTestOneInput(FuzzedDataProvider data) { try { + var tLogs = Tuf.transparencyLogsFrom(data); byte[] byteArray = data.consumeRemainingAsBytes(); String string = new String(byteArray, StandardCharsets.UTF_8); URI uri = new URI(URL); RekorEntry entry = RekorResponse.newRekorResponse(uri, string).getEntry(); - RekorVerifier verifier = RekorVerifier.newRekorVerifier(byteArray); + RekorVerifier verifier = RekorVerifier.newRekorVerifier(tLogs); verifier.verifyEntry(entry); verifier.verifyInclusionProof(entry); - } catch (URISyntaxException - | InvalidKeySpecException - | NoSuchAlgorithmException - | IOException - | RekorParseException - | RekorVerificationException e) { + } catch (URISyntaxException | RekorParseException | RekorVerificationException e) { // Known exception } } diff --git a/fuzzing/src/main/java/util/Tuf.java b/fuzzing/src/main/java/util/Tuf.java new file mode 100644 index 000000000..4c2909c6a --- /dev/null +++ b/fuzzing/src/main/java/util/Tuf.java @@ -0,0 +1,93 @@ +/* + * 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 util; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.google.common.hash.Hashing; +import dev.sigstore.trustroot.CertificateAuthorities; +import dev.sigstore.trustroot.CertificateAuthority; +import dev.sigstore.trustroot.ImmutableCertificateAuthorities; +import dev.sigstore.trustroot.ImmutableCertificateAuthority; +import dev.sigstore.trustroot.ImmutableLogId; +import dev.sigstore.trustroot.ImmutablePublicKey; +import dev.sigstore.trustroot.ImmutableSubject; +import dev.sigstore.trustroot.ImmutableTransparencyLog; +import dev.sigstore.trustroot.ImmutableTransparencyLogs; +import dev.sigstore.trustroot.ImmutableValidFor; +import dev.sigstore.trustroot.TransparencyLog; +import dev.sigstore.trustroot.TransparencyLogs; +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.security.cert.CertPath; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public final class Tuf { + + // arbitrarily decided max certificate size in bytes + private static final int MAX_CERT_SIZE = 10240; + + // ecdsa key size in bytes + private static final int ECDSA_KEY_BYTES = 91; + + public static TransparencyLogs transparencyLogsFrom(FuzzedDataProvider data) { + return ImmutableTransparencyLogs.builder().addTransparencyLog(genTlog(data)).build(); + } + + public static CertificateAuthorities certificateAuthoritiesFrom(FuzzedDataProvider data) + throws CertificateException { + return ImmutableCertificateAuthorities.builder().addCertificateAuthority(genCA(data)).build(); + } + + private static CertPath genCertPath(FuzzedDataProvider data) throws CertificateException { + List certList = new ArrayList(); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + certList.add( + cf.generateCertificate(new ByteArrayInputStream(data.consumeBytes(MAX_CERT_SIZE)))); + certList.add( + cf.generateCertificate(new ByteArrayInputStream(data.consumeBytes(MAX_CERT_SIZE)))); + return cf.generateCertPath(certList); + } + + private static CertificateAuthority genCA(FuzzedDataProvider data) throws CertificateException { + return ImmutableCertificateAuthority.builder() + .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) + .subject(ImmutableSubject.builder().commonName("test").organization("test").build()) + .certPath(genCertPath(data)) + .uri(URI.create("test")) + .build(); + } + + private static TransparencyLog genTlog(FuzzedDataProvider data) { + var pk = + ImmutablePublicKey.builder() + .keyDetails("PKIX_ECDSA_P256_SHA_256") + .rawBytes(data.consumeBytes(ECDSA_KEY_BYTES)) + .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) + .build(); + var logId = Hashing.sha256().hashBytes(pk.getRawBytes()).asBytes(); + return ImmutableTransparencyLog.builder() + .baseUrl(URI.create("test")) + .hashAlgorithm("SHA2_256") + .publicKey(pk) + .logId(ImmutableLogId.builder().keyId(logId).build()) + .build(); + } +} diff --git a/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/work/SignWorkAction.kt b/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/work/SignWorkAction.kt index a49accdb9..359ca0d19 100644 --- a/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/work/SignWorkAction.kt +++ b/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/work/SignWorkAction.kt @@ -19,6 +19,7 @@ package dev.sigstore.sign.work import dev.sigstore.KeylessSigner import dev.sigstore.bundle.BundleFactory import dev.sigstore.oidc.client.OidcClient +import dev.sigstore.oidc.client.OidcClients import dev.sigstore.sign.OidcClientConfiguration import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property @@ -50,8 +51,7 @@ abstract class SignWorkAction : WorkAction { val signer = clients.computeIfAbsent(oidcClient.key()) { KeylessSigner.builder().apply { sigstorePublicDefaults() - @Suppress("DEPRECATION") - oidcClient(oidcClient.build() as OidcClient) + oidcClients(OidcClients.of(oidcClient.build() as OidcClient)) }.build() } diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java index 26baab781..d2745bd8f 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java @@ -21,19 +21,26 @@ import com.google.common.hash.Hashing; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; -import com.google.errorprone.annotations.InlineMe; import com.google.errorprone.annotations.concurrent.GuardedBy; import dev.sigstore.encryption.certificates.Certificates; import dev.sigstore.encryption.signers.Signer; import dev.sigstore.encryption.signers.Signers; -import dev.sigstore.fulcio.client.*; -import dev.sigstore.oidc.client.OidcClient; +import dev.sigstore.fulcio.client.CertificateRequest; +import dev.sigstore.fulcio.client.FulcioClient; +import dev.sigstore.fulcio.client.FulcioVerificationException; +import dev.sigstore.fulcio.client.FulcioVerifier; +import dev.sigstore.fulcio.client.SigningCertificate; +import dev.sigstore.fulcio.client.UnsupportedAlgorithmException; import dev.sigstore.oidc.client.OidcClients; import dev.sigstore.oidc.client.OidcException; import dev.sigstore.oidc.client.OidcToken; -import dev.sigstore.rekor.client.*; +import dev.sigstore.rekor.client.HashedRekordRequest; +import dev.sigstore.rekor.client.RekorClient; +import dev.sigstore.rekor.client.RekorParseException; +import dev.sigstore.rekor.client.RekorVerificationException; +import dev.sigstore.rekor.client.RekorVerifier; +import dev.sigstore.tuf.SigstoreTufClient; import java.io.IOException; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.InvalidAlgorithmParameterException; @@ -120,37 +127,17 @@ public static Builder builder() { } public static class Builder { - private FulcioClient fulcioClient; - private FulcioVerifier fulcioVerifier; - private RekorClient rekorClient; - private RekorVerifier rekorVerifier; + private SigstoreTufClient sigstoreTufClient; private OidcClients oidcClients; private Signer signer; private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME; @CanIgnoreReturnValue - public Builder fulcioClient(FulcioClient fulcioClient, FulcioVerifier fulcioVerifier) { - this.fulcioClient = fulcioClient; - this.fulcioVerifier = fulcioVerifier; + public Builder sigstoreTufClient(SigstoreTufClient sigstoreTufClient) { + this.sigstoreTufClient = sigstoreTufClient; return this; } - @CanIgnoreReturnValue - public Builder rekorClient(RekorClient rekorClient, RekorVerifier rekorVerifier) { - this.rekorClient = rekorClient; - this.rekorVerifier = rekorVerifier; - return this; - } - - @CanIgnoreReturnValue - @Deprecated - @InlineMe( - replacement = "this.oidcClients(OidcClients.of(oidcClient))", - imports = "dev.sigstore.oidc.client.OidcClients") - public final Builder oidcClient(OidcClient oidcClient) { - return oidcClients(OidcClients.of(oidcClient)); - } - @CanIgnoreReturnValue public Builder oidcClients(OidcClients oidcClients) { this.oidcClients = oidcClients; @@ -182,13 +169,16 @@ public Builder minSigningCertificateLifetime(Duration minSigningCertificateLifet } @CheckReturnValue - public KeylessSigner build() { - Preconditions.checkNotNull(fulcioClient, "fulcioClient"); - Preconditions.checkNotNull(fulcioVerifier, "fulcioVerifier"); - Preconditions.checkNotNull(rekorClient, "rekorClient"); - Preconditions.checkNotNull(rekorVerifier, "rekorVerifier"); - Preconditions.checkNotNull(oidcClients, "oidcClients"); - Preconditions.checkNotNull(signer, "signer"); + public KeylessSigner build() + throws CertificateException, IOException, NoSuchAlgorithmException, InvalidKeySpecException, + InvalidKeyException, InvalidAlgorithmParameterException { + Preconditions.checkNotNull(sigstoreTufClient, "sigstoreTufClient"); + sigstoreTufClient.update(); + var trustedRoot = sigstoreTufClient.getSigstoreTrustedRoot(); + var fulcioClient = FulcioClient.builder().setCertificateAuthority(trustedRoot).build(); + var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot); + var rekorClient = RekorClient.builder().setTransparencyLog(trustedRoot).build(); + var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot); return new KeylessSigner( fulcioClient, fulcioVerifier, @@ -199,38 +189,26 @@ public KeylessSigner build() { minSigningCertificateLifetime); } + /** + * Initialize a builder with the sigstore public good instance tuf root and oidc targets with + * ecdsa signing. + */ @CanIgnoreReturnValue - public Builder sigstorePublicDefaults() - throws IOException, InvalidAlgorithmParameterException, CertificateException, - InvalidKeySpecException, NoSuchAlgorithmException { - fulcioClient( - FulcioClient.builder().build(), - FulcioVerifier.newFulcioVerifier( - VerificationMaterial.Production.fulioCert(), - VerificationMaterial.Production.ctfePublicKeys())); - rekorClient( - RekorClient.builder().build(), - RekorVerifier.newRekorVerifier(VerificationMaterial.Production.rekorPublicKey())); + public Builder sigstorePublicDefaults() throws IOException, NoSuchAlgorithmException { + sigstoreTufClient = SigstoreTufClient.builder().usePublicGoodInstance().build(); oidcClients(OidcClients.DEFAULTS); signer(Signers.newEcdsaSigner()); minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME); return this; } + /** + * Initialize a builder with the sigstore staging instance tuf root and oidc targets with ecdsa + * signing. + */ @CanIgnoreReturnValue - public Builder sigstoreStagingDefaults() - throws IOException, InvalidAlgorithmParameterException, CertificateException, - InvalidKeySpecException, NoSuchAlgorithmException { - fulcioClient( - FulcioClient.builder() - .setServerUrl(URI.create(FulcioClient.STAGING_FULCIO_SERVER)) - .build(), - FulcioVerifier.newFulcioVerifier( - VerificationMaterial.Staging.fulioCert(), - VerificationMaterial.Staging.ctfePublicKeys())); - rekorClient( - RekorClient.builder().setServerUrl(URI.create(RekorClient.STAGING_REKOR_SERVER)).build(), - RekorVerifier.newRekorVerifier(VerificationMaterial.Staging.rekorPublicKey())); + public Builder sigstoreStagingDefaults() throws IOException, NoSuchAlgorithmException { + sigstoreTufClient = SigstoreTufClient.builder().useStagingInstance().build(); oidcClients(OidcClients.STAGING_DEFAULTS); signer(Signers.newEcdsaSigner()); minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME); @@ -251,7 +229,7 @@ public List sign(List artifactDigests) throws OidcException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedAlgorithmException, CertificateException, IOException, FulcioVerificationException, RekorVerificationException, InterruptedException, - RekorParseException { + RekorParseException, InvalidKeySpecException { if (artifactDigests.size() == 0) { throw new IllegalArgumentException("Require one or more digests"); @@ -350,7 +328,8 @@ private void renewSigningCertificate() public KeylessSignature sign(byte[] artifactDigest) throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException, CertificateException, NoSuchAlgorithmException, SignatureException, IOException, - OidcException, InvalidKeyException, InterruptedException, RekorParseException { + OidcException, InvalidKeyException, InterruptedException, RekorParseException, + InvalidKeySpecException { return sign(List.of(artifactDigest)).get(0); } @@ -364,7 +343,8 @@ public KeylessSignature sign(byte[] artifactDigest) public Map signFiles(List artifacts) throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException, CertificateException, NoSuchAlgorithmException, SignatureException, IOException, - OidcException, InvalidKeyException, InterruptedException, RekorParseException { + OidcException, InvalidKeyException, InterruptedException, RekorParseException, + InvalidKeySpecException { if (artifacts.size() == 0) { throw new IllegalArgumentException("Require one or more paths"); } @@ -391,7 +371,8 @@ public Map signFiles(List artifacts) public KeylessSignature signFile(Path artifact) throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException, CertificateException, NoSuchAlgorithmException, SignatureException, IOException, - OidcException, InvalidKeyException, InterruptedException, RekorParseException { + OidcException, InvalidKeyException, InterruptedException, RekorParseException, + InvalidKeySpecException { return signFiles(List.of(artifact)).get(artifact); } } diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java deleted file mode 100644 index 2bedb6d02..000000000 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright 2022 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; - -import com.google.api.client.util.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.hash.Hashing; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.CheckReturnValue; -import com.google.errorprone.annotations.concurrent.GuardedBy; -import dev.sigstore.encryption.certificates.Certificates; -import dev.sigstore.encryption.signers.Signer; -import dev.sigstore.encryption.signers.Signers; -import dev.sigstore.fulcio.client.CertificateRequest; -import dev.sigstore.fulcio.client.FulcioClient2; -import dev.sigstore.fulcio.client.FulcioVerificationException; -import dev.sigstore.fulcio.client.FulcioVerifier2; -import dev.sigstore.fulcio.client.SigningCertificate; -import dev.sigstore.fulcio.client.UnsupportedAlgorithmException; -import dev.sigstore.oidc.client.OidcClients; -import dev.sigstore.oidc.client.OidcException; -import dev.sigstore.oidc.client.OidcToken; -import dev.sigstore.rekor.client.HashedRekordRequest; -import dev.sigstore.rekor.client.RekorClient2; -import dev.sigstore.rekor.client.RekorParseException; -import dev.sigstore.rekor.client.RekorVerificationException; -import dev.sigstore.rekor.client.RekorVerifier2; -import dev.sigstore.tuf.SigstoreTufClient; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.cert.CertificateException; -import java.security.spec.InvalidKeySpecException; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * A full sigstore keyless signing flow. - * - *

Note: the implementation is thread-safe assuming the clients (Fulcio, OIDC, Rekor) are - * thread-safe - */ -public class KeylessSigner2 implements AutoCloseable { - /** - * The instance of the {@link KeylessSigner2} will try to reuse a previously acquired certificate - * if the expiration time on the certificate is more than {@code minSigningCertificateLifetime} - * time away. Otherwise, it will make a new request (OIDC, Fulcio) to obtain a new updated - * certificate to use for signing. This is a default value for the remaining lifetime of the - * signing certificate that is considered good enough. - */ - public static final Duration DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME = Duration.ofMinutes(5); - - private final FulcioClient2 fulcioClient; - private final FulcioVerifier2 fulcioVerifier; - private final RekorClient2 rekorClient; - private final RekorVerifier2 rekorVerifier; - private final OidcClients oidcClients; - private final Signer signer; - private final Duration minSigningCertificateLifetime; - - /** The code signing certificate from Fulcio. */ - @GuardedBy("lock") - private @Nullable SigningCertificate signingCert; - - /** - * Representation {@link #signingCert} in PEM bytes format. This is used to avoid serializing the - * certificate for each use. - */ - @GuardedBy("lock") - private byte @Nullable [] signingCertPemBytes; - - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - - private KeylessSigner2( - FulcioClient2 fulcioClient, - FulcioVerifier2 fulcioVerifier, - RekorClient2 rekorClient, - RekorVerifier2 rekorVerifier, - OidcClients oidcClients, - Signer signer, - Duration minSigningCertificateLifetime) { - this.fulcioClient = fulcioClient; - this.fulcioVerifier = fulcioVerifier; - this.rekorClient = rekorClient; - this.rekorVerifier = rekorVerifier; - this.oidcClients = oidcClients; - this.signer = signer; - this.minSigningCertificateLifetime = minSigningCertificateLifetime; - } - - @Override - public void close() { - lock.writeLock().lock(); - try { - signingCert = null; - signingCertPemBytes = null; - } finally { - lock.writeLock().unlock(); - } - } - - @CheckReturnValue - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private SigstoreTufClient sigstoreTufClient; - private OidcClients oidcClients; - private Signer signer; - private Duration minSigningCertificateLifetime = DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME; - - @CanIgnoreReturnValue - public Builder sigstoreTufClient(SigstoreTufClient sigstoreTufClient) { - this.sigstoreTufClient = sigstoreTufClient; - return this; - } - - @CanIgnoreReturnValue - public Builder oidcClients(OidcClients oidcClients) { - this.oidcClients = oidcClients; - return this; - } - - @CanIgnoreReturnValue - public Builder signer(Signer signer) { - this.signer = signer; - return this; - } - - /** - * The instance of the {@link KeylessSigner2} will try to reuse a previously acquired - * certificate if the expiration time on the certificate is more than {@code - * minSigningCertificateLifetime} time away. Otherwise, it will make a new request (OIDC, - * Fulcio) to obtain a new updated certificate to use for signing. Default {@code - * minSigningCertificateLifetime} is {@link #DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME}". - * - * @param minSigningCertificateLifetime the minimum lifetime of the signing certificate before - * renewal - * @return this builder - * @see Fulcio certificate validity - */ - @CanIgnoreReturnValue - public Builder minSigningCertificateLifetime(Duration minSigningCertificateLifetime) { - this.minSigningCertificateLifetime = minSigningCertificateLifetime; - return this; - } - - @CheckReturnValue - public KeylessSigner2 build() - throws CertificateException, IOException, NoSuchAlgorithmException, InvalidKeySpecException, - InvalidKeyException, InvalidAlgorithmParameterException { - Preconditions.checkNotNull(sigstoreTufClient, "sigstoreTufClient"); - sigstoreTufClient.update(); - var trustedRoot = sigstoreTufClient.getSigstoreTrustedRoot(); - var fulcioClient = FulcioClient2.builder().setCertificateAuthority(trustedRoot).build(); - var fulcioVerifier = FulcioVerifier2.newFulcioVerifier(trustedRoot); - var rekorClient = RekorClient2.builder().setTransparencyLog(trustedRoot).build(); - var rekorVerifier = RekorVerifier2.newRekorVerifier(trustedRoot); - return new KeylessSigner2( - fulcioClient, - fulcioVerifier, - rekorClient, - rekorVerifier, - oidcClients, - signer, - minSigningCertificateLifetime); - } - - /** - * Initialize a builder with the sigstore public good instance tuf root and oidc targets with - * ecdsa signing. - */ - @CanIgnoreReturnValue - public Builder sigstorePublicDefaults() throws IOException, NoSuchAlgorithmException { - sigstoreTufClient = SigstoreTufClient.builder().usePublicGoodInstance().build(); - oidcClients(OidcClients.DEFAULTS); - signer(Signers.newEcdsaSigner()); - minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME); - return this; - } - - /** - * Initialize a builder with the sigstore staging instance tuf root and oidc targets with ecdsa - * signing. - */ - @CanIgnoreReturnValue - public Builder sigstoreStagingDefaults() throws IOException, NoSuchAlgorithmException { - sigstoreTufClient = SigstoreTufClient.builder().useStagingInstance().build(); - oidcClients(OidcClients.STAGING_DEFAULTS); - signer(Signers.newEcdsaSigner()); - minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME); - return this; - } - } - - /** - * Sign one or more artifact digests using the keyless signing workflow. The oidc/fulcio dance to - * obtain a signing certificate will only occur once. The same ephemeral private key will be used - * to sign all artifacts. This method will renew certificates as they expire. - * - * @param artifactDigests sha256 digests of the artifacts to sign. - * @return a list of keyless singing results. - */ - @CheckReturnValue - public List sign(List artifactDigests) - throws OidcException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, - UnsupportedAlgorithmException, CertificateException, IOException, - FulcioVerificationException, RekorVerificationException, InterruptedException, - RekorParseException, InvalidKeySpecException { - - if (artifactDigests.size() == 0) { - throw new IllegalArgumentException("Require one or more digests"); - } - - var result = ImmutableList.builder(); - - for (var artifactDigest : artifactDigests) { - var signature = signer.signDigest(artifactDigest); - - // Technically speaking, it is unlikely the certificate will expire between signing artifacts - // However, files might be large, and it might take time to talk to Rekor - // so we check the certificate expiration here. - renewSigningCertificate(); - SigningCertificate signingCert; - byte[] signingCertPemBytes; - lock.readLock().lock(); - try { - signingCert = this.signingCert; - signingCertPemBytes = this.signingCertPemBytes; - if (signingCert == null) { - throw new IllegalStateException("Signing certificate is null"); - } - } finally { - lock.readLock().unlock(); - } - - var rekorRequest = - HashedRekordRequest.newHashedRekordRequest( - artifactDigest, signingCertPemBytes, signature); - var rekorResponse = rekorClient.putEntry(rekorRequest); - rekorVerifier.verifyEntry(rekorResponse.getEntry()); - - result.add( - ImmutableKeylessSignature.builder() - .digest(artifactDigest) - .certPath(signingCert.getCertPath()) - .signature(signature) - .entry(rekorResponse.getEntry()) - .build()); - } - return result.build(); - } - - private void renewSigningCertificate() - throws InterruptedException, CertificateException, IOException, UnsupportedAlgorithmException, - NoSuchAlgorithmException, InvalidKeyException, SignatureException, - FulcioVerificationException, OidcException { - // Check if the certificate is still valid - lock.readLock().lock(); - try { - if (signingCert != null) { - @SuppressWarnings("JavaUtilDate") - long lifetimeLeft = - signingCert.getLeafCertificate().getNotAfter().getTime() - System.currentTimeMillis(); - if (lifetimeLeft > minSigningCertificateLifetime.toMillis()) { - // The current certificate is fine, reuse it - return; - } - } - } finally { - lock.readLock().unlock(); - } - - // Renew Fulcio certificate - lock.writeLock().lock(); - try { - signingCert = null; - signingCertPemBytes = null; - OidcToken tokenInfo = oidcClients.getIDToken(); - SigningCertificate signingCert = - fulcioClient.signingCertificate( - CertificateRequest.newCertificateRequest( - signer.getPublicKey(), - tokenInfo.getIdToken(), - signer.sign( - tokenInfo.getSubjectAlternativeName().getBytes(StandardCharsets.UTF_8)))); - fulcioVerifier.verifyCertChain(signingCert); - // TODO: this signing workflow mandates SCTs, but fulcio itself doesn't, figure out a way to - // allow that to be known - fulcioVerifier.verifySct(signingCert); - this.signingCert = signingCert; - signingCertPemBytes = Certificates.toPemBytes(signingCert.getLeafCertificate()); - } finally { - lock.writeLock().unlock(); - } - } - - /** - * Convenience wrapper around {@link #sign(List)} to sign a single digest - * - * @param artifactDigest sha256 digest of the artifact to sign. - * @return a keyless singing results. - */ - @CheckReturnValue - public KeylessSignature sign(byte[] artifactDigest) - throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException, - CertificateException, NoSuchAlgorithmException, SignatureException, IOException, - OidcException, InvalidKeyException, InterruptedException, RekorParseException, - InvalidKeySpecException { - return sign(List.of(artifactDigest)).get(0); - } - - /** - * Convenience wrapper around {@link #sign(List)} to accept files instead of digests - * - * @param artifacts list of the artifacts to sign. - * @return a map of artifacts and their keyless singing results. - */ - @CheckReturnValue - public Map signFiles(List artifacts) - throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException, - CertificateException, NoSuchAlgorithmException, SignatureException, IOException, - OidcException, InvalidKeyException, InterruptedException, RekorParseException, - InvalidKeySpecException { - if (artifacts.size() == 0) { - throw new IllegalArgumentException("Require one or more paths"); - } - var digests = new ArrayList(artifacts.size()); - for (var artifact : artifacts) { - var artifactByteSource = com.google.common.io.Files.asByteSource(artifact.toFile()); - digests.add(artifactByteSource.hash(Hashing.sha256()).asBytes()); - } - var signingResult = sign(digests); - var result = ImmutableMap.builder(); - for (int i = 0; i < artifacts.size(); i++) { - result.put(artifacts.get(i), signingResult.get(i)); - } - return result.build(); - } - - /** - * Convenience wrapper around {@link #sign(List)} to accept a file instead of digests - * - * @param artifact the artifacts to sign. - * @return a keyless singing results. - */ - @CheckReturnValue - public KeylessSignature signFile(Path artifact) - throws FulcioVerificationException, RekorVerificationException, UnsupportedAlgorithmException, - CertificateException, NoSuchAlgorithmException, SignatureException, IOException, - OidcException, InvalidKeyException, InterruptedException, RekorParseException, - InvalidKeySpecException { - return signFiles(List.of(artifact)).get(artifact); - } -} diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index 9d70dbd69..d8c9b742b 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java @@ -25,11 +25,19 @@ import dev.sigstore.fulcio.client.FulcioVerificationException; import dev.sigstore.fulcio.client.FulcioVerifier; import dev.sigstore.fulcio.client.SigningCertificate; -import dev.sigstore.rekor.client.*; +import dev.sigstore.rekor.client.HashedRekordRequest; +import dev.sigstore.rekor.client.RekorClient; +import dev.sigstore.rekor.client.RekorEntry; +import dev.sigstore.rekor.client.RekorParseException; +import dev.sigstore.rekor.client.RekorVerificationException; +import dev.sigstore.rekor.client.RekorVerifier; +import dev.sigstore.tuf.SigstoreTufClient; import java.io.IOException; -import java.net.URI; import java.nio.file.Path; -import java.security.*; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; @@ -59,52 +67,27 @@ public static KeylessVerifier.Builder builder() { } public static class Builder { - private FulcioVerifier fulcioVerifier; - private RekorClient rekorClient; - private RekorVerifier rekorVerifier; + private SigstoreTufClient sigstoreTufClient; - public KeylessVerifier.Builder fulcioVerifier(FulcioVerifier fulcioVerifier) { - this.fulcioVerifier = fulcioVerifier; - return this; - } - - public KeylessVerifier.Builder rekorClient( - RekorClient rekorClient, RekorVerifier rekorVerifier) { - this.rekorClient = rekorClient; - this.rekorVerifier = rekorVerifier; - return this; - } - - public KeylessVerifier build() { - Preconditions.checkNotNull(fulcioVerifier); - Preconditions.checkNotNull(rekorVerifier); - Preconditions.checkNotNull(rekorClient); + public KeylessVerifier build() + throws InvalidAlgorithmParameterException, CertificateException, InvalidKeySpecException, + NoSuchAlgorithmException, IOException, InvalidKeyException { + Preconditions.checkNotNull(sigstoreTufClient); + sigstoreTufClient.update(); + var trustedRoot = sigstoreTufClient.getSigstoreTrustedRoot(); + var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustedRoot); + var rekorClient = RekorClient.builder().setTransparencyLog(trustedRoot).build(); + var rekorVerifier = RekorVerifier.newRekorVerifier(trustedRoot); return new KeylessVerifier(fulcioVerifier, rekorClient, rekorVerifier); } - public Builder sigstorePublicDefaults() - throws IOException, InvalidAlgorithmParameterException, CertificateException, - InvalidKeySpecException, NoSuchAlgorithmException { - fulcioVerifier( - FulcioVerifier.newFulcioVerifier( - VerificationMaterial.Production.fulioCert(), - VerificationMaterial.Production.ctfePublicKeys())); - rekorClient( - RekorClient.builder().build(), - RekorVerifier.newRekorVerifier(VerificationMaterial.Production.rekorPublicKey())); + public Builder sigstorePublicDefaults() throws IOException { + sigstoreTufClient = SigstoreTufClient.builder().usePublicGoodInstance().build(); return this; } - public Builder sigstoreStagingDefaults() - throws IOException, InvalidAlgorithmParameterException, CertificateException, - InvalidKeySpecException, NoSuchAlgorithmException { - fulcioVerifier( - FulcioVerifier.newFulcioVerifier( - VerificationMaterial.Staging.fulioCert(), - VerificationMaterial.Staging.ctfePublicKeys())); - rekorClient( - RekorClient.builder().setServerUrl(URI.create(RekorClient.STAGING_REKOR_SERVER)).build(), - RekorVerifier.newRekorVerifier(VerificationMaterial.Staging.rekorPublicKey())); + public Builder sigstoreStagingDefaults() throws IOException { + sigstoreTufClient = SigstoreTufClient.builder().useStagingInstance().build(); return this; } } @@ -177,7 +160,7 @@ public void verify(byte[] artifactDigest, KeylessVerificationRequest request) // verify the certificate chains up to a trusted root (fulcio) try { fulcioVerifier.verifyCertChain(signingCert); - } catch (FulcioVerificationException ex) { + } catch (FulcioVerificationException | IOException ex) { throw new KeylessVerificationException( "Fulcio certificate was not valid: " + ex.getMessage(), ex); } diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java deleted file mode 100644 index 6fe1af56d..000000000 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2022 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; - -import com.google.api.client.util.Preconditions; -import com.google.common.hash.Hashing; -import com.google.common.io.Files; -import dev.sigstore.KeylessVerificationRequest.VerificationOptions; -import dev.sigstore.encryption.certificates.Certificates; -import dev.sigstore.encryption.signers.Verifiers; -import dev.sigstore.fulcio.client.FulcioCertificateVerifier; -import dev.sigstore.fulcio.client.FulcioVerificationException; -import dev.sigstore.fulcio.client.FulcioVerifier2; -import dev.sigstore.fulcio.client.SigningCertificate; -import dev.sigstore.rekor.client.HashedRekordRequest; -import dev.sigstore.rekor.client.RekorClient2; -import dev.sigstore.rekor.client.RekorEntry; -import dev.sigstore.rekor.client.RekorParseException; -import dev.sigstore.rekor.client.RekorVerificationException; -import dev.sigstore.rekor.client.RekorVerifier2; -import dev.sigstore.tuf.SigstoreTufClient; -import java.io.IOException; -import java.nio.file.Path; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.spec.InvalidKeySpecException; -import java.sql.Date; -import java.time.Instant; -import java.util.Arrays; -import java.util.Optional; -import org.bouncycastle.util.encoders.Hex; - -/** Verify hashrekords from rekor signed using the keyless signing flow with fulcio certificates. */ -public class KeylessVerifier2 { - private final FulcioVerifier2 fulcioVerifier; - private final RekorVerifier2 rekorVerifier; - private final RekorClient2 rekorClient; - - private KeylessVerifier2( - FulcioVerifier2 fulcioVerifier, RekorClient2 rekorClient, RekorVerifier2 rekorVerifier) { - this.fulcioVerifier = fulcioVerifier; - this.rekorClient = rekorClient; - this.rekorVerifier = rekorVerifier; - } - - public static KeylessVerifier2.Builder builder() { - return new KeylessVerifier2.Builder(); - } - - public static class Builder { - private SigstoreTufClient sigstoreTufClient; - - public KeylessVerifier2 build() - throws InvalidAlgorithmParameterException, CertificateException, InvalidKeySpecException, - NoSuchAlgorithmException, IOException, InvalidKeyException { - Preconditions.checkNotNull(sigstoreTufClient); - sigstoreTufClient.update(); - var trustedRoot = sigstoreTufClient.getSigstoreTrustedRoot(); - var fulcioVerifier = FulcioVerifier2.newFulcioVerifier(trustedRoot); - var rekorClient = RekorClient2.builder().setTransparencyLog(trustedRoot).build(); - var rekorVerifier = RekorVerifier2.newRekorVerifier(trustedRoot); - return new KeylessVerifier2(fulcioVerifier, rekorClient, rekorVerifier); - } - - public Builder sigstorePublicDefaults() throws IOException { - sigstoreTufClient = SigstoreTufClient.builder().usePublicGoodInstance().build(); - return this; - } - - public Builder sigstoreStagingDefaults() throws IOException { - sigstoreTufClient = SigstoreTufClient.builder().useStagingInstance().build(); - return this; - } - } - - /** - * Verify that the inputs can attest to the validity of a signature using sigstore's keyless - * infrastructure. If no exception is thrown, it should be assumed verification has passed. - * - * @param artifactDigest the sha256 digest of the artifact that was signed - * @param certChain the certificate chain obtained from a fulcio instance - * @param signature the signature on the artifact - * @throws KeylessVerificationException if the signing information could not be verified - */ - @Deprecated - public void verifyOnline(byte[] artifactDigest, byte[] certChain, byte[] signature) - throws KeylessVerificationException { - try { - verify( - artifactDigest, - KeylessVerificationRequest.builder() - .keylessSignature( - KeylessSignature.builder() - .signature(signature) - .certPath(Certificates.fromPemChain(certChain)) - .digest(artifactDigest) - .build()) - .verificationOptions(VerificationOptions.builder().isOnline(true).build()) - .build()); - } catch (CertificateException ex) { - throw new KeylessVerificationException("Certificate was not valid: " + ex.getMessage(), ex); - } - } - - /** Convenience wrapper around {@link #verify(byte[], KeylessVerificationRequest)}. */ - public void verify(Path artifact, KeylessVerificationRequest request) - throws KeylessVerificationException { - try { - byte[] artifactDigest = - Files.asByteSource(artifact.toFile()).hash(Hashing.sha256()).asBytes(); - verify(artifactDigest, request); - } catch (IOException e) { - throw new KeylessVerificationException("Could not hash provided artifact path: " + artifact); - } - } - - /** - * Verify that the inputs can attest to the validity of a signature using sigstore's keyless - * infrastructure. If no exception is thrown, it should be assumed verification has passed. - * - * @param artifactDigest the sha256 digest of the artifact that is being verified - * @param request the keyless verification data and options - * @throws KeylessVerificationException if the signing information could not be verified - */ - public void verify(byte[] artifactDigest, KeylessVerificationRequest request) - throws KeylessVerificationException { - var signingCert = SigningCertificate.from(request.getKeylessSignature().getCertPath()); - var leafCert = signingCert.getLeafCertificate(); - - // this ensures the provided artifact digest matches what may have come from a bundle (in - // keyless signature) - if (!Arrays.equals(artifactDigest, request.getKeylessSignature().getDigest())) { - throw new KeylessVerificationException( - "Provided artifact sha256 digest does not match digest used for verification" - + "\nprovided(hex) : " - + Hex.toHexString(artifactDigest) - + "\nverification : " - + Hex.toHexString(request.getKeylessSignature().getDigest())); - } - - // verify the certificate chains up to a trusted root (fulcio) - try { - fulcioVerifier.verifyCertChain(signingCert); - } catch (FulcioVerificationException | IOException ex) { - throw new KeylessVerificationException( - "Fulcio certificate was not valid: " + ex.getMessage(), ex); - } - - // make the sure a crt is signed by the certificate transparency log (embedded only) - try { - fulcioVerifier.verifySct(signingCert); - } catch (FulcioVerificationException ex) { - throw new KeylessVerificationException( - "Fulcio certificate SCT was not valid: " + ex.getMessage(), ex); - } - - // verify the certificate identity if options are present - if (request.getVerificationOptions().getCertificateIdentities().size() > 0) { - try { - new FulcioCertificateVerifier() - .verifyCertificateMatches( - leafCert, request.getVerificationOptions().getCertificateIdentities()); - } catch (FulcioVerificationException fve) { - throw new KeylessVerificationException( - "Could not verify certificate identities: " + fve.getMessage(), fve); - } - } - - var signature = request.getKeylessSignature().getSignature(); - - var rekorEntry = - request.getVerificationOptions().isOnline() - ? getEntryFromRekor(artifactDigest, leafCert, signature) - : request - .getKeylessSignature() - .getEntry() - .orElseThrow( - () -> - new KeylessVerificationException( - "No rekor entry was provided for offline verification")); - - // verify the rekor entry is signed by the log keys - try { - rekorVerifier.verifyEntry(rekorEntry); - } catch (RekorVerificationException ex) { - throw new KeylessVerificationException("Rekor entry signature was not valid"); - } - - // verify any inclusion proof - if (rekorEntry.getVerification().getInclusionProof().isPresent()) { - try { - rekorVerifier.verifyInclusionProof(rekorEntry); - } catch (RekorVerificationException ex) { - throw new KeylessVerificationException("Rekor entry inclusion proof was not valid"); - } - } else if (request.getVerificationOptions().isOnline()) { - throw new KeylessVerificationException("Fetched rekor entry did not contain inclusion proof"); - } - - // check if the time of entry inclusion in the log (a stand-in for signing time) is within the - // validity period for the certificate - var entryTime = Date.from(Instant.ofEpochSecond(rekorEntry.getIntegratedTime())); - try { - leafCert.checkValidity(entryTime); - } catch (CertificateNotYetValidException e) { - throw new KeylessVerificationException("Signing time was before certificate validity", e); - } catch (CertificateExpiredException e) { - throw new KeylessVerificationException("Signing time was after certificate expiry", e); - } - - // finally check the supplied signature can be verified by the public key in the certificate - var publicKey = leafCert.getPublicKey(); - try { - var verifier = Verifiers.newVerifier(publicKey); - if (!verifier.verifyDigest(artifactDigest, signature)) { - throw new KeylessVerificationException("Artifact signature was not valid"); - } - } catch (NoSuchAlgorithmException | InvalidKeyException ex) { - throw new RuntimeException(ex); - } catch (SignatureException ex) { - throw new KeylessVerificationException( - "Signature could not be processed: " + ex.getMessage(), ex); - } - } - - private RekorEntry getEntryFromRekor( - byte[] artifactDigest, Certificate leafCert, byte[] signature) - throws KeylessVerificationException { - // rebuild the hashedRekord so we can query the log for it - HashedRekordRequest hashedRekordRequest = null; - try { - hashedRekordRequest = - HashedRekordRequest.newHashedRekordRequest( - artifactDigest, Certificates.toPemBytes(leafCert), signature); - } catch (IOException e) { - throw new KeylessVerificationException( - "Could not convert certificate to PEM when recreating hashrekord", e); - } - Optional rekorEntry; - - // attempt to grab the rekord from the rekor instance - try { - rekorEntry = rekorClient.getEntry(hashedRekordRequest); - if (rekorEntry.isEmpty()) { - throw new KeylessVerificationException("Rekor entry was not found"); - } - } catch (IOException | RekorParseException e) { - throw new KeylessVerificationException("Could not retrieve rekor entry", e); - } - return rekorEntry.get(); - } -} diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java index 7f783ec71..ba878434e 100644 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java @@ -15,44 +15,40 @@ */ package dev.sigstore.fulcio.client; -import static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.*; +import static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.SIGNED_CERTIFICATE_DETACHED_SCT; import com.google.protobuf.ByteString; -import dev.sigstore.encryption.certificates.transparency.SerializationException; -import dev.sigstore.fulcio.v2.*; +import dev.sigstore.fulcio.v2.CAGrpc; +import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest; +import dev.sigstore.fulcio.v2.Credentials; +import dev.sigstore.fulcio.v2.PublicKey; +import dev.sigstore.fulcio.v2.PublicKeyRequest; import dev.sigstore.http.GrpcChannels; import dev.sigstore.http.HttpParams; import dev.sigstore.http.ImmutableHttpParams; -import java.io.IOException; -import java.net.URI; +import dev.sigstore.trustroot.CertificateAuthority; +import dev.sigstore.trustroot.SigstoreTrustedRoot; import java.security.cert.CertificateException; import java.util.Base64; import java.util.concurrent.TimeUnit; /** A client to communicate with a fulcio service instance over gRPC. */ public class FulcioClient { - // GRPC explicitly doesn't want https:// in the server address, so it is not included - public static final String PUBLIC_FULCIO_SERVER = "fulcio.sigstore.dev"; - public static final String STAGING_FULCIO_SERVER = "fulcio.sigstage.dev"; - public static final boolean DEFAULT_REQUIRE_SCT = true; private final HttpParams httpParams; - private final URI serverUrl; - private final boolean requireSct; + private final CertificateAuthority certificateAuthority; public static Builder builder() { return new Builder(); } - private FulcioClient(HttpParams httpParams, URI serverUrl, boolean requireSct) { - this.serverUrl = serverUrl; - this.requireSct = requireSct; + private FulcioClient(HttpParams httpParams, CertificateAuthority certificateAuthority) { + this.certificateAuthority = certificateAuthority; this.httpParams = httpParams; } public static class Builder { - private URI serverUrl = URI.create(PUBLIC_FULCIO_SERVER); - private boolean requireSct = DEFAULT_REQUIRE_SCT; + private CertificateAuthority certificateAuthority; private HttpParams httpParams = ImmutableHttpParams.builder().build(); private Builder() {} @@ -63,26 +59,20 @@ public Builder setHttpParams(HttpParams httpParams) { return this; } - /** - * The fulcio remote server URI, defaults to {@value PUBLIC_FULCIO_SERVER}. Do not include - * http:// or https:// in the server URL. - */ - public Builder setServerUrl(URI uri) { - this.serverUrl = uri; + /** The remote fulcio instance. */ + public Builder setCertificateAuthority(CertificateAuthority certificateAuthority) { + this.certificateAuthority = certificateAuthority; return this; } - /** - * Configure whether we should expect the fulcio instance to return an sct with the signing - * certificate, defaults to {@value DEFAULT_REQUIRE_SCT}. - */ - public Builder requireSct(boolean requireSct) { - this.requireSct = requireSct; + /** The remote fulcio instance inferred from a trustedRoot. */ + public Builder setCertificateAuthority(SigstoreTrustedRoot trustedRoot) { + this.certificateAuthority = trustedRoot.getCAs().current(); return this; } public FulcioClient build() { - return new FulcioClient(httpParams, serverUrl, requireSct); + return new FulcioClient(httpParams, certificateAuthority); } } @@ -93,11 +83,17 @@ public FulcioClient build() { * @return a {@link SigningCertificate} from fulcio */ public SigningCertificate signingCertificate(CertificateRequest request) - throws InterruptedException, CertificateException, IOException { - // TODO: If we want to reduce the cost of creating channels/connections, we could try + throws InterruptedException, CertificateException { + if (!certificateAuthority.isCurrent()) { + throw new RuntimeException( + "Certificate Authority '" + certificateAuthority.getUri() + "' is not current"); + } + // TODO: 1. If we want to reduce the cost of creating channels/connections, we could try // to make a new connection once per batch of fulcio requests, but we're not really // at that point yet. - var channel = GrpcChannels.newManagedChannel(serverUrl, httpParams); + // TODO: 2. getUri().getAuthority() is potentially prone to error if we don't get a good URI + var channel = + GrpcChannels.newManagedChannel(certificateAuthority.getUri().getAuthority(), httpParams); try { var client = CAGrpc.newBlockingStub(channel); @@ -128,20 +124,9 @@ public SigningCertificate signingCertificate(CertificateRequest request) .createSigningCertificate(req); if (certs.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) { - if (certs.getSignedCertificateDetachedSct().getSignedCertificateTimestamp().isEmpty() - && requireSct) { - throw new CertificateException( - "no signed certificate timestamps were found in response from Fulcio"); - } - try { - return SigningCertificate.newSigningCertificate(certs.getSignedCertificateDetachedSct()); - } catch (SerializationException se) { - throw new CertificateException("Could not parse detached SCT"); - } - } else { - return SigningCertificate.newSigningCertificate(certs.getSignedCertificateEmbeddedSct()); + throw new CertificateException("Detached SCTs are not supported"); } - + return SigningCertificate.newSigningCertificate(certs.getSignedCertificateEmbeddedSct()); } finally { channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient2.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient2.java deleted file mode 100644 index d51a0df15..000000000 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient2.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2022 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.fulcio.client; - -import static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.SIGNED_CERTIFICATE_DETACHED_SCT; - -import com.google.protobuf.ByteString; -import dev.sigstore.fulcio.v2.CAGrpc; -import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest; -import dev.sigstore.fulcio.v2.Credentials; -import dev.sigstore.fulcio.v2.PublicKey; -import dev.sigstore.fulcio.v2.PublicKeyRequest; -import dev.sigstore.http.GrpcChannels; -import dev.sigstore.http.HttpParams; -import dev.sigstore.http.ImmutableHttpParams; -import dev.sigstore.trustroot.CertificateAuthority; -import dev.sigstore.trustroot.SigstoreTrustedRoot; -import java.security.cert.CertificateException; -import java.util.Base64; -import java.util.concurrent.TimeUnit; - -/** A client to communicate with a fulcio service instance over gRPC. */ -public class FulcioClient2 { - - private final HttpParams httpParams; - private final CertificateAuthority certificateAuthority; - - public static Builder builder() { - return new Builder(); - } - - private FulcioClient2(HttpParams httpParams, CertificateAuthority certificateAuthority) { - this.certificateAuthority = certificateAuthority; - this.httpParams = httpParams; - } - - public static class Builder { - private CertificateAuthority certificateAuthority; - private HttpParams httpParams = ImmutableHttpParams.builder().build(); - - private Builder() {} - - /** Configure the http properties, see {@link HttpParams}. */ - public Builder setHttpParams(HttpParams httpParams) { - this.httpParams = httpParams; - return this; - } - - /** The remote fulcio instance. */ - public Builder setCertificateAuthority(CertificateAuthority certificateAuthority) { - this.certificateAuthority = certificateAuthority; - return this; - } - - /** The remote fulcio instance inferred from a trustedRoot. */ - public Builder setCertificateAuthority(SigstoreTrustedRoot trustedRoot) { - this.certificateAuthority = trustedRoot.getCAs().current(); - return this; - } - - public FulcioClient2 build() { - return new FulcioClient2(httpParams, certificateAuthority); - } - } - - /** - * Request a signing certificate from fulcio. - * - * @param request certificate request parameters - * @return a {@link SigningCertificate} from fulcio - */ - public SigningCertificate signingCertificate(CertificateRequest request) - throws InterruptedException, CertificateException { - if (!certificateAuthority.isCurrent()) { - throw new RuntimeException( - "Certificate Authority '" + certificateAuthority.getUri() + "' is not current"); - } - // TODO: 1. If we want to reduce the cost of creating channels/connections, we could try - // to make a new connection once per batch of fulcio requests, but we're not really - // at that point yet. - // TODO: 2. getUri().getAuthority() is potentially prone to error if we don't get a good URI - var channel = - GrpcChannels.newManagedChannel(certificateAuthority.getUri().getAuthority(), httpParams); - - try { - var client = CAGrpc.newBlockingStub(channel); - var credentials = Credentials.newBuilder().setOidcIdentityToken(request.getIdToken()).build(); - - String pemEncodedPublicKey = - "-----BEGIN PUBLIC KEY-----\n" - + Base64.getEncoder().encodeToString(request.getPublicKey().getEncoded()) - + "\n-----END PUBLIC KEY-----"; - var publicKeyRequest = - PublicKeyRequest.newBuilder() - .setPublicKey( - PublicKey.newBuilder() - .setAlgorithm(request.getPublicKeyAlgorithm()) - .setContent(pemEncodedPublicKey) - .build()) - .setProofOfPossession(ByteString.copyFrom(request.getProofOfPossession())) - .build(); - var req = - CreateSigningCertificateRequest.newBuilder() - .setCredentials(credentials) - .setPublicKeyRequest(publicKeyRequest) - .build(); - - var certs = - client - .withDeadlineAfter(httpParams.getTimeout(), TimeUnit.SECONDS) - .createSigningCertificate(req); - - if (certs.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) { - throw new CertificateException("Detached SCTs are not supported"); - } - return SigningCertificate.newSigningCertificate(certs.getSignedCertificateEmbeddedSct()); - } finally { - channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); - } - } -} diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java index c82e2ed5c..e0dd1fb59 100644 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java @@ -15,72 +15,75 @@ */ package dev.sigstore.fulcio.client; -import dev.sigstore.encryption.Keys; -import dev.sigstore.encryption.certificates.transparency.*; -import java.io.ByteArrayInputStream; +import dev.sigstore.encryption.certificates.Certificates; +import dev.sigstore.encryption.certificates.transparency.CTLogInfo; +import dev.sigstore.encryption.certificates.transparency.CTVerificationResult; +import dev.sigstore.encryption.certificates.transparency.CTVerifier; +import dev.sigstore.trustroot.CertificateAuthorities; +import dev.sigstore.trustroot.SigstoreTrustedRoot; +import dev.sigstore.trustroot.TransparencyLogs; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.cert.*; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.PKIXParameters; import java.security.spec.InvalidKeySpecException; -import java.util.*; -import org.checkerframework.checker.nullness.qual.Nullable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -/** Verifier for fulcio {@link dev.sigstore.fulcio.client.SigningCertificate}. */ +/** Verifier for fulcio {@link SigningCertificate}. */ public class FulcioVerifier { - private final @Nullable CTVerifier ctVerifier; - private final TrustAnchor fulcioRoot; + private final CertificateAuthorities cas; + private final TransparencyLogs ctLogs; + private final CTVerifier ctVerifier; - /** - * Instantiate a new verifier. - * - * @param fulcioRoot fulcio's root certificate - * @param ctfePublicKeys fulcio's certificate transparency log public keys (for all logs) - */ - public static FulcioVerifier newFulcioVerifier(byte[] fulcioRoot, List ctfePublicKeys) - throws InvalidKeySpecException, NoSuchAlgorithmException, CertificateException, IOException, - InvalidAlgorithmParameterException { - - List ctfePublicKeyObjs = null; - if (ctfePublicKeys != null && ctfePublicKeys.size() != 0) { - ctfePublicKeyObjs = new ArrayList<>(); - for (var pk : ctfePublicKeys) { - ctfePublicKeyObjs.add(Keys.parsePublicKey(pk)); - } - } + public static FulcioVerifier newFulcioVerifier(SigstoreTrustedRoot trustRoot) + throws InvalidAlgorithmParameterException, CertificateException, InvalidKeySpecException, + NoSuchAlgorithmException { + return newFulcioVerifier(trustRoot.getCAs(), trustRoot.getCTLogs()); + } - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - X509Certificate fulcioRootObj = - (X509Certificate) - certificateFactory.generateCertificate(new ByteArrayInputStream(fulcioRoot)); + public static FulcioVerifier newFulcioVerifier( + CertificateAuthorities cas, TransparencyLogs ctLogs) + throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, + CertificateException { + List logs = new ArrayList<>(); + for (var ctLog : ctLogs.all()) { + logs.add( + new CTLogInfo( + ctLog.getPublicKey().toJavaPublicKey(), "CT Log", ctLog.getBaseUrl().toString())); + } + var verifier = + new CTVerifier( + logId -> + logs.stream() + .filter(ctLogInfo -> Arrays.equals(ctLogInfo.getID(), logId)) + .findFirst() + .orElse(null)); - TrustAnchor fulcioRootTrustAnchor = new TrustAnchor(fulcioRootObj, null); - // this should throw an InvalidAlgorithmException a bit earlier that would otherwise be - // encountered in verifyCertPath - new PKIXParameters(Collections.singleton(fulcioRootTrustAnchor)); + // check to see if we can use all fulcio roots (this is a bit eager) + for (var ca : cas.all()) { + ca.asTrustAnchor(); + } - return new FulcioVerifier(fulcioRootTrustAnchor, ctfePublicKeyObjs); + return new FulcioVerifier(cas, ctLogs, verifier); } - private FulcioVerifier(TrustAnchor fulcioRoot, @Nullable List ctfePublicKeys) { - this.fulcioRoot = fulcioRoot; - if (ctfePublicKeys != null) { - var logInfos = new ArrayList(); - for (var pk : ctfePublicKeys) { - var ctLogInfo = new CTLogInfo(pk, "fulcio ct log", "unused-url"); - logInfos.add(ctLogInfo); - } - this.ctVerifier = - new CTVerifier( - logId -> - logInfos.stream() - .filter(ctLogInfo -> Arrays.equals(ctLogInfo.getID(), logId)) - .findFirst() - .orElse(null)); - } else { - ctVerifier = null; - } + private FulcioVerifier( + CertificateAuthorities cas, TransparencyLogs ctLogs, CTVerifier ctVerifier) { + this.cas = cas; + this.ctLogs = ctLogs; + this.ctVerifier = ctVerifier; } /** @@ -91,46 +94,52 @@ private FulcioVerifier(TrustAnchor fulcioRoot, @Nullable List ctfePub * @throws FulcioVerificationException if verification fails for any reason */ public void verifySct(SigningCertificate signingCertificate) throws FulcioVerificationException { - if (ctVerifier == null) { - throw new FulcioVerificationException("No ct-log public key was provided to verifier"); + if (ctLogs.size() == 0) { + throw new FulcioVerificationException("No ct logs were provided to verifier"); } if (signingCertificate.getDetachedSct().isPresent()) { - CertificateEntry ce; - try { - ce = CertificateEntry.createForX509Certificate(signingCertificate.getLeafCertificate()); - } catch (CertificateEncodingException cee) { - throw new FulcioVerificationException("Leaf certificate could not be parsed", cee); - } + throw new FulcioVerificationException( + "Detached SCTs are not supported for validating certificates"); + } else if (signingCertificate.getEmbeddedSct().isPresent()) { + verifyEmbeddedScts(signingCertificate); + } else { + throw new FulcioVerificationException("No valid SCTs were found during verification"); + } + } - var status = ctVerifier.verifySingleSCT(signingCertificate.getDetachedSct().get(), ce); - if (status != VerifiedSCT.Status.VALID) { - throw new FulcioVerificationException( - "SCT could not be verified because " + status.toString()); - } - } else if (signingCertificate.hasEmbeddedSct()) { - var certs = signingCertificate.getCertificates(); - CTVerificationResult result; - try { - // even though we're sending the whole chain, this method only checks SCTs on the leaf cert - result = ctVerifier.verifySignedCertificateTimestamps(certs, null, null); - } catch (CertificateEncodingException cee) { - throw new FulcioVerificationException( - "Certificates could not be parsed during sct verification"); - } - int valid = result.getValidSCTs().size(); - int invalid = result.getInvalidSCTs().size(); - if (valid == 0 || invalid != 0) { - throw new FulcioVerificationException( - "Expecting at least one valid sct, but found " - + valid - + " valid and " - + invalid - + " invalid scts"); + private void verifyEmbeddedScts(SigningCertificate signingCertificate) + throws FulcioVerificationException { + var certs = signingCertificate.getCertificates(); + CTVerificationResult result; + try { + // even though we're sending the whole chain, this method only checks SCTs on the leaf cert + result = ctVerifier.verifySignedCertificateTimestamps(certs, null, null); + } catch (CertificateEncodingException cee) { + throw new FulcioVerificationException( + "Certificates could not be parsed during SCT verification"); + } + + // these are technically valid, but we have the additional constraint of sigstore's trustroot + // providing a validity period for logs, so make sure all SCTs were signed by a log during + // that log's validity period + for (var validSct : result.getValidSCTs()) { + var sct = validSct.sct; + + var logId = sct.getLogID(); + var entryTime = Instant.ofEpochMilli(sct.getTimestamp()); + + var ctLog = ctLogs.find(logId, entryTime); + if (ctLog.isPresent()) { + // TODO: currently we only require one valid SCT, but maybe this should be configurable? + // found at least one valid sct with a matching valid log + return; } - } else { - throw new FulcioVerificationException("No detached or embedded SCTs were found to verify"); } + throw new FulcioVerificationException( + "No valid SCTs were found, all(" + + (result.getValidSCTs().size() + result.getInvalidSCTs().size()) + + ") SCTs were invalid"); } /** @@ -141,7 +150,7 @@ public void verifySct(SigningCertificate signingCertificate) throws FulcioVerifi * @throws FulcioVerificationException if verification fails for any reason */ public void verifyCertChain(SigningCertificate signingCertificate) - throws FulcioVerificationException { + throws FulcioVerificationException, IOException { CertPathValidator cpv; try { cpv = CertPathValidator.getInstance("PKIX"); @@ -152,27 +161,55 @@ public void verifyCertChain(SigningCertificate signingCertificate) e); } - PKIXParameters pkixParams; - try { - pkixParams = new PKIXParameters(Collections.singleton(fulcioRoot)); - } catch (InvalidAlgorithmParameterException e) { - throw new RuntimeException( - "Can't create PKIX parameters for fulcioRoot. This should have been checked when generating a verifier instance", - e); + var leaf = signingCertificate.getLeafCertificate(); + var validCAs = cas.find(leaf.getNotBefore().toInstant()); + + if (validCAs.size() == 0) { + throw new FulcioVerificationException( + "No valid Certificate Authorities found when validating certificate"); } - pkixParams.setRevocationEnabled(false); - // these certs are only valid for 15 minutes, so find a time in the validity period - @SuppressWarnings("JavaUtilDate") - Date dateInValidityPeriod = - new Date(signingCertificate.getLeafCertificate().getNotBefore().getTime()); - pkixParams.setDate(dateInValidityPeriod); + Map caVerificationFailure = new LinkedHashMap<>(); - try { - // a result is returned here, but I don't know what to do with it yet - cpv.validate(signingCertificate.getCertPath(), pkixParams); - } catch (CertPathValidatorException | InvalidAlgorithmParameterException ve) { - throw new FulcioVerificationException(ve); + System.out.println(Certificates.toPemString(signingCertificate.getCertPath())); + + for (var ca : validCAs) { + PKIXParameters pkixParams; + try { + pkixParams = new PKIXParameters(Collections.singleton(ca.asTrustAnchor())); + } catch (InvalidAlgorithmParameterException | CertificateException e) { + throw new RuntimeException( + "Can't create PKIX parameters for fulcioRoot. This should have been checked when generating a verifier instance", + e); + } + pkixParams.setRevocationEnabled(false); + + // these certs are only valid for 15 minutes, so find a time in the validity period + @SuppressWarnings("JavaUtilDate") + Date dateInValidityPeriod = + new Date(signingCertificate.getLeafCertificate().getNotBefore().getTime()); + pkixParams.setDate(dateInValidityPeriod); + + try { + // build a cert chain with the root-chain in question and the provided leaf + var rebuiltCert = + Certificates.appendCertPath(ca.getCertPath(), signingCertificate.getLeafCertificate()); + // a result is returned here, but we ignore it + cpv.validate(rebuiltCert, pkixParams); + } catch (CertPathValidatorException + | InvalidAlgorithmParameterException + | CertificateException ve) { + caVerificationFailure.put(ca.getUri().toString(), ve.getMessage()); + // verification failed + continue; + } + // verification passed so just end this method + return; } + String errors = + caVerificationFailure.entrySet().stream() + .map(entry -> entry.getKey() + " (" + entry.getValue() + ")") + .collect(Collectors.joining("\n")); + throw new FulcioVerificationException("Certificate was not verifiable against CAs\n" + errors); } } diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier2.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier2.java deleted file mode 100644 index 8a85f2179..000000000 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier2.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2022 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.fulcio.client; - -import dev.sigstore.encryption.certificates.Certificates; -import dev.sigstore.encryption.certificates.transparency.CTLogInfo; -import dev.sigstore.encryption.certificates.transparency.CTVerificationResult; -import dev.sigstore.encryption.certificates.transparency.CTVerifier; -import dev.sigstore.trustroot.CertificateAuthorities; -import dev.sigstore.trustroot.SigstoreTrustedRoot; -import dev.sigstore.trustroot.TransparencyLogs; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertPathValidator; -import java.security.cert.CertPathValidatorException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.PKIXParameters; -import java.security.spec.InvalidKeySpecException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** Verifier for fulcio {@link SigningCertificate}. */ -public class FulcioVerifier2 { - private final CertificateAuthorities cas; - private final TransparencyLogs ctLogs; - private final CTVerifier ctVerifier; - - public static FulcioVerifier2 newFulcioVerifier(SigstoreTrustedRoot trustRoot) - throws InvalidAlgorithmParameterException, CertificateException, InvalidKeySpecException, - NoSuchAlgorithmException { - return newFulcioVerifier(trustRoot.getCAs(), trustRoot.getCTLogs()); - } - - public static FulcioVerifier2 newFulcioVerifier( - CertificateAuthorities cas, TransparencyLogs ctLogs) - throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - CertificateException { - List logs = new ArrayList<>(); - for (var ctLog : ctLogs.all()) { - logs.add( - new CTLogInfo( - ctLog.getPublicKey().toJavaPublicKey(), "CT Log", ctLog.getBaseUrl().toString())); - } - var verifier = - new CTVerifier( - logId -> - logs.stream() - .filter(ctLogInfo -> Arrays.equals(ctLogInfo.getID(), logId)) - .findFirst() - .orElse(null)); - - // check to see if we can use all fulcio roots (this is a bit eager) - for (var ca : cas.all()) { - ca.asTrustAnchor(); - } - - return new FulcioVerifier2(cas, ctLogs, verifier); - } - - private FulcioVerifier2( - CertificateAuthorities cas, TransparencyLogs ctLogs, CTVerifier ctVerifier) { - this.cas = cas; - this.ctLogs = ctLogs; - this.ctVerifier = ctVerifier; - } - - /** - * Verify that an SCT associated with a Singing Certificate is valid and signed by the configured - * CT-log public key. - * - * @param signingCertificate containing the SCT metadata to verify - * @throws FulcioVerificationException if verification fails for any reason - */ - public void verifySct(SigningCertificate signingCertificate) throws FulcioVerificationException { - if (ctLogs.size() == 0) { - throw new FulcioVerificationException("No ct logs were provided to verifier"); - } - - if (signingCertificate.getDetachedSct().isPresent()) { - throw new FulcioVerificationException( - "Detached SCTs are not supported for validating certificates"); - } else if (signingCertificate.getEmbeddedSct().isPresent()) { - verifyEmbeddedScts(signingCertificate); - } else { - throw new FulcioVerificationException("No valid SCTs were found during verification"); - } - } - - private void verifyEmbeddedScts(SigningCertificate signingCertificate) - throws FulcioVerificationException { - var certs = signingCertificate.getCertificates(); - CTVerificationResult result; - try { - // even though we're sending the whole chain, this method only checks SCTs on the leaf cert - result = ctVerifier.verifySignedCertificateTimestamps(certs, null, null); - } catch (CertificateEncodingException cee) { - throw new FulcioVerificationException( - "Certificates could not be parsed during SCT verification"); - } - - // these are technically valid, but we have the additional constraint of sigstore's trustroot - // providing a validity period for logs, so make sure all SCTs were signed by a log during - // that log's validity period - for (var validSct : result.getValidSCTs()) { - var sct = validSct.sct; - - var logId = sct.getLogID(); - var entryTime = Instant.ofEpochMilli(sct.getTimestamp()); - - var ctLog = ctLogs.find(logId, entryTime); - if (ctLog.isPresent()) { - // TODO: currently we only require one valid SCT, but maybe this should be configurable? - // found at least one valid sct with a matching valid log - return; - } - } - throw new FulcioVerificationException( - "No valid SCTs were found, all(" - + (result.getValidSCTs().size() + result.getInvalidSCTs().size()) - + ") SCTs were invalid"); - } - - /** - * Verify that a cert chain is valid and chains up to the trust anchor (fulcio public key) - * configured in this validator. - * - * @param signingCertificate containing the certificate chain - * @throws FulcioVerificationException if verification fails for any reason - */ - public void verifyCertChain(SigningCertificate signingCertificate) - throws FulcioVerificationException, IOException { - CertPathValidator cpv; - try { - cpv = CertPathValidator.getInstance("PKIX"); - } catch (NoSuchAlgorithmException e) { - // - throw new RuntimeException( - "No PKIX CertPathValidator, we probably shouldn't be here, but this seems to be a system library error not a program control flow issue", - e); - } - - var leaf = signingCertificate.getLeafCertificate(); - var validCAs = cas.find(leaf.getNotBefore().toInstant()); - - if (validCAs.size() == 0) { - throw new FulcioVerificationException( - "No valid Certificate Authorities found when validating certificate"); - } - - Map caVerificationFailure = new LinkedHashMap<>(); - - System.out.println(Certificates.toPemString(signingCertificate.getCertPath())); - - for (var ca : validCAs) { - PKIXParameters pkixParams; - try { - pkixParams = new PKIXParameters(Collections.singleton(ca.asTrustAnchor())); - } catch (InvalidAlgorithmParameterException | CertificateException e) { - throw new RuntimeException( - "Can't create PKIX parameters for fulcioRoot. This should have been checked when generating a verifier instance", - e); - } - pkixParams.setRevocationEnabled(false); - - // these certs are only valid for 15 minutes, so find a time in the validity period - @SuppressWarnings("JavaUtilDate") - Date dateInValidityPeriod = - new Date(signingCertificate.getLeafCertificate().getNotBefore().getTime()); - pkixParams.setDate(dateInValidityPeriod); - - try { - // build a cert chain with the root-chain in question and the provided leaf - var rebuiltCert = - Certificates.appendCertPath(ca.getCertPath(), signingCertificate.getLeafCertificate()); - // a result is returned here, but we ignore it - cpv.validate(rebuiltCert, pkixParams); - } catch (CertPathValidatorException - | InvalidAlgorithmParameterException - | CertificateException ve) { - caVerificationFailure.put(ca.getUri().toString(), ve.getMessage()); - // verification failed - continue; - } - // verification passed so just end this method - return; - } - String errors = - caVerificationFailure.entrySet().stream() - .map(entry -> entry.getKey() + " (" + entry.getValue() + ")") - .collect(Collectors.joining("\n")); - throw new FulcioVerificationException("Certificate was not verifiable against CAs\n" + errors); - } -} diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient.java index e35339582..aad151c94 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient.java @@ -17,53 +17,69 @@ import static dev.sigstore.json.GsonSupplier.GSON; -import com.google.api.client.http.*; +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.common.base.Preconditions; import dev.sigstore.http.HttpClients; import dev.sigstore.http.HttpParams; import dev.sigstore.http.ImmutableHttpParams; +import dev.sigstore.trustroot.SigstoreTrustedRoot; +import dev.sigstore.trustroot.TransparencyLog; import java.io.IOException; import java.net.URI; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Optional; /** A client to communicate with a rekor service instance. */ public class RekorClient { - public static final String PUBLIC_REKOR_SERVER = "https://rekor.sigstore.dev"; - public static final String STAGING_REKOR_SERVER = "https://rekor.sigstage.dev"; public static final String REKOR_ENTRIES_PATH = "/api/v1/log/entries"; public static final String REKOR_INDEX_SEARCH_PATH = "/api/v1/index/retrieve"; private final HttpParams httpParams; - private final URI serverUrl; + private final TransparencyLog tlog; public static RekorClient.Builder builder() { return new RekorClient.Builder(); } - private RekorClient(HttpParams httpParams, URI serverUrl) { - this.serverUrl = serverUrl; + private RekorClient(HttpParams httpParams, TransparencyLog tlog) { + this.tlog = tlog; this.httpParams = httpParams; } public static class Builder { - private URI serverUrl = URI.create(PUBLIC_REKOR_SERVER); private HttpParams httpParams = ImmutableHttpParams.builder().build(); + private TransparencyLog tlog; private Builder() {} /** Configure the http properties, see {@link HttpParams}, {@link ImmutableHttpParams}. */ - public RekorClient.Builder setHttpParams(HttpParams httpParams) { + public Builder setHttpParams(HttpParams httpParams) { this.httpParams = httpParams; return this; } - /** The fulcio remote server URI, defaults to {@value PUBLIC_REKOR_SERVER}. */ - public RekorClient.Builder setServerUrl(URI uri) { - this.serverUrl = uri; + /** Configure the remote rekor instance to communicate with. */ + public Builder setTransparencyLog(TransparencyLog tlog) { + this.tlog = tlog; + return this; + } + + /** Configure the remote rekor instance to communicate with, inferred from a trusted root. */ + public Builder setTransparencyLog(SigstoreTrustedRoot trustedRoot) { + this.tlog = trustedRoot.getTLogs().current(); return this; } public RekorClient build() { - return new RekorClient(httpParams, serverUrl); + Preconditions.checkNotNull(tlog); + return new RekorClient(httpParams, tlog); } } @@ -75,7 +91,7 @@ public RekorClient build() { */ public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest) throws IOException, RekorParseException { - URI rekorPutEndpoint = serverUrl.resolve(REKOR_ENTRIES_PATH); + URI rekorPutEndpoint = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH); HttpRequest req = HttpClients.newRequestFactory(httpParams) @@ -96,7 +112,7 @@ public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest) resp.parseAsString())); } - URI rekorEntryUri = serverUrl.resolve(resp.getHeaders().getLocation()); + URI rekorEntryUri = tlog.getBaseUrl().resolve(resp.getHeaders().getLocation()); String entry = resp.parseAsString(); return RekorResponse.newRekorResponse(rekorEntryUri, entry); } @@ -107,7 +123,7 @@ public Optional getEntry(HashedRekordRequest hashedRekordRequest) } public Optional getEntry(String UUID) throws IOException, RekorParseException { - URI getEntryURI = serverUrl.resolve(REKOR_ENTRIES_PATH + "/" + UUID); + URI getEntryURI = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH + "/" + UUID); HttpRequest req = HttpClients.newRequestFactory(httpParams).buildGetRequest(new GenericUrl(getEntryURI)); req.getHeaders().set("Accept", "application/json"); @@ -133,7 +149,7 @@ public Optional getEntry(String UUID) throws IOException, RekorParse public List searchEntry( String email, String hash, String publicKeyFormat, String publicKeyContent) throws IOException { - URI rekorSearchEndpoint = serverUrl.resolve(REKOR_INDEX_SEARCH_PATH); + URI rekorSearchEndpoint = tlog.getBaseUrl().resolve(REKOR_INDEX_SEARCH_PATH); HashMap publicKeyParams = null; if (publicKeyContent != null) { diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient2.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient2.java deleted file mode 100644 index ea5330df1..000000000 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorClient2.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2022 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.rekor.client; - -import static dev.sigstore.json.GsonSupplier.GSON; - -import com.google.api.client.http.ByteArrayContent; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; -import com.google.common.base.Preconditions; -import dev.sigstore.http.HttpClients; -import dev.sigstore.http.HttpParams; -import dev.sigstore.http.ImmutableHttpParams; -import dev.sigstore.trustroot.SigstoreTrustedRoot; -import dev.sigstore.trustroot.TransparencyLog; -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -/** A client to communicate with a rekor service instance. */ -public class RekorClient2 { - public static final String REKOR_ENTRIES_PATH = "/api/v1/log/entries"; - public static final String REKOR_INDEX_SEARCH_PATH = "/api/v1/index/retrieve"; - - private final HttpParams httpParams; - private final TransparencyLog tlog; - - public static RekorClient2.Builder builder() { - return new RekorClient2.Builder(); - } - - private RekorClient2(HttpParams httpParams, TransparencyLog tlog) { - this.tlog = tlog; - this.httpParams = httpParams; - } - - public static class Builder { - private HttpParams httpParams = ImmutableHttpParams.builder().build(); - private TransparencyLog tlog; - - private Builder() {} - - /** Configure the http properties, see {@link HttpParams}, {@link ImmutableHttpParams}. */ - public Builder setHttpParams(HttpParams httpParams) { - this.httpParams = httpParams; - return this; - } - - /** Configure the remote rekor instance to communicate with. */ - public Builder setTransparencyLog(TransparencyLog tlog) { - this.tlog = tlog; - return this; - } - - /** Configure the remote rekor instance to communicate with, inferred from a trusted root. */ - public Builder setTransparencyLog(SigstoreTrustedRoot trustedRoot) { - this.tlog = trustedRoot.getTLogs().current(); - return this; - } - - public RekorClient2 build() { - Preconditions.checkNotNull(tlog); - return new RekorClient2(httpParams, tlog); - } - } - - /** - * Put a new hashedrekord entry on the Rekor log. - * - * @param hashedRekordRequest the request to send to rekor - * @return a {@link RekorResponse} with information about the log entry - */ - public RekorResponse putEntry(HashedRekordRequest hashedRekordRequest) - throws IOException, RekorParseException { - URI rekorPutEndpoint = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH); - - HttpRequest req = - HttpClients.newRequestFactory(httpParams) - .buildPostRequest( - new GenericUrl(rekorPutEndpoint), - ByteArrayContent.fromString( - "application/json", hashedRekordRequest.toJsonPayload())); - req.getHeaders().set("Accept", "application/json"); - req.getHeaders().set("Content-Type", "application/json"); - - HttpResponse resp = req.execute(); - if (resp.getStatusCode() != 201) { - throw new IOException( - String.format( - Locale.ROOT, - "bad response from rekor @ '%s' : %s", - rekorPutEndpoint, - resp.parseAsString())); - } - - URI rekorEntryUri = tlog.getBaseUrl().resolve(resp.getHeaders().getLocation()); - String entry = resp.parseAsString(); - return RekorResponse.newRekorResponse(rekorEntryUri, entry); - } - - public Optional getEntry(HashedRekordRequest hashedRekordRequest) - throws IOException, RekorParseException { - return getEntry(hashedRekordRequest.computeUUID()); - } - - public Optional getEntry(String UUID) throws IOException, RekorParseException { - URI getEntryURI = tlog.getBaseUrl().resolve(REKOR_ENTRIES_PATH + "/" + UUID); - HttpRequest req = - HttpClients.newRequestFactory(httpParams).buildGetRequest(new GenericUrl(getEntryURI)); - req.getHeaders().set("Accept", "application/json"); - HttpResponse response; - try { - response = req.execute(); - } catch (HttpResponseException e) { - if (e.getStatusCode() == 404) return Optional.empty(); - throw e; - } - return Optional.of( - RekorResponse.newRekorResponse(getEntryURI, response.parseAsString()).getEntry()); - } - - /** - * Returns a list of UUIDs for matching entries for the given search parameters. - * - * @param email the OIDC email subject - * @param hash sha256 hash of the artifact - * @param publicKeyFormat format of public key (one of 'pgp','x509','minisign', 'ssh', 'tuf') - * @param publicKeyContent public key base64 encoded content - */ - public List searchEntry( - String email, String hash, String publicKeyFormat, String publicKeyContent) - throws IOException { - URI rekorSearchEndpoint = tlog.getBaseUrl().resolve(REKOR_INDEX_SEARCH_PATH); - - HashMap publicKeyParams = null; - if (publicKeyContent != null) { - publicKeyParams = new HashMap<>(); - publicKeyParams.put("format", publicKeyFormat); - publicKeyParams.put("content", publicKeyContent); - } - var data = new HashMap(); - data.put("email", email); - data.put("hash", hash); - data.put("publicKey", publicKeyParams); - - String contentString = GSON.get().toJson(data); - HttpRequest req = - HttpClients.newRequestFactory(httpParams) - .buildPostRequest( - new GenericUrl(rekorSearchEndpoint), - ByteArrayContent.fromString("application/json", contentString)); - req.getHeaders().set("Accept", "application/json"); - req.getHeaders().set("Content-Type", "application/json"); - var response = req.execute(); - return Arrays.asList(GSON.get().fromJson(response.parseAsString(), String[].class)); - } -} diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java index ab3cf7907..4a2ea9b7f 100644 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier.java @@ -16,34 +16,31 @@ package dev.sigstore.rekor.client; import com.google.common.hash.Hashing; -import dev.sigstore.encryption.Keys; -import dev.sigstore.encryption.signers.Verifier; import dev.sigstore.encryption.signers.Verifiers; -import java.io.IOException; -import java.security.*; +import dev.sigstore.trustroot.SigstoreTrustedRoot; +import dev.sigstore.trustroot.TransparencyLogs; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; +import java.time.Instant; import java.util.Base64; import org.bouncycastle.util.encoders.Hex; /** Verifier for rekor entries. */ public class RekorVerifier { - private final Verifier verifier; + private final TransparencyLogs tlogs; - // A calculated logId from the transparency log (rekor) public key - private final String calculatedLogId; - - public static RekorVerifier newRekorVerifier(byte[] rekorPublicKey) - throws InvalidKeySpecException, NoSuchAlgorithmException, IOException { - var publicKey = Keys.parsePublicKey(rekorPublicKey); - var verifier = Verifiers.newVerifier(publicKey); + public static RekorVerifier newRekorVerifier(SigstoreTrustedRoot trustRoot) { + return newRekorVerifier(trustRoot.getTLogs()); + } - return new RekorVerifier(verifier); + public static RekorVerifier newRekorVerifier(TransparencyLogs tlogs) { + return new RekorVerifier(tlogs); } - private RekorVerifier(Verifier verifier) { - this.calculatedLogId = - Hashing.sha256().hashBytes(verifier.getPublicKey().getEncoded()).toString(); - this.verifier = verifier; + private RekorVerifier(TransparencyLogs tlogs) { + this.tlogs = tlogs; } /** @@ -61,16 +58,23 @@ public void verifyEntry(RekorEntry entry) throws RekorVerificationException { throw new RekorVerificationException("No signed entry timestamp found in entry."); } - if (!entry.getLogID().equals(calculatedLogId)) { - throw new RekorVerificationException("LogId does not match supplied rekor public key."); - } + var tlog = + tlogs + .find(Hex.decode(entry.getLogID()), Instant.ofEpochSecond(entry.getIntegratedTime())) + .orElseThrow( + () -> + new RekorVerificationException( + "Log entry (logid, timestamp) does not match any provided transparency logs.")); try { + var verifier = Verifiers.newVerifier(tlog.getPublicKey().toJavaPublicKey()); if (!verifier.verify( entry.getSignableContent(), Base64.getDecoder().decode(entry.getVerification().getSignedEntryTimestamp()))) { throw new RekorVerificationException("Entry SET was not valid"); } + } catch (InvalidKeySpecException ike) { + throw new RekorVerificationException("Public Key could be parsed", ike); } catch (InvalidKeyException ike) { throw new RekorVerificationException("Public Key was invalid", ike); } catch (SignatureException se) { diff --git a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier2.java b/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier2.java deleted file mode 100644 index b660e329b..000000000 --- a/sigstore-java/src/main/java/dev/sigstore/rekor/client/RekorVerifier2.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2022 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.rekor.client; - -import com.google.common.hash.Hashing; -import dev.sigstore.encryption.signers.Verifiers; -import dev.sigstore.trustroot.SigstoreTrustedRoot; -import dev.sigstore.trustroot.TransparencyLogs; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.time.Instant; -import java.util.Base64; -import org.bouncycastle.util.encoders.Hex; - -/** Verifier for rekor entries. */ -public class RekorVerifier2 { - private final TransparencyLogs tlogs; - - public static RekorVerifier2 newRekorVerifier(SigstoreTrustedRoot trustRoot) { - return newRekorVerifier(trustRoot.getTLogs()); - } - - public static RekorVerifier2 newRekorVerifier(TransparencyLogs tlogs) { - return new RekorVerifier2(tlogs); - } - - private RekorVerifier2(TransparencyLogs tlogs) { - this.tlogs = tlogs; - } - - /** - * Verify that a Rekor Entry is signed with the rekor public key loaded into this verifier - * - * @param entry the entry to verify - * @throws RekorVerificationException if the entry cannot be verified - */ - public void verifyEntry(RekorEntry entry) throws RekorVerificationException { - if (entry.getVerification() == null) { - throw new RekorVerificationException("No verification information in entry."); - } - - if (entry.getVerification().getSignedEntryTimestamp() == null) { - throw new RekorVerificationException("No signed entry timestamp found in entry."); - } - - var tlog = - tlogs - .find(Hex.decode(entry.getLogID()), Instant.ofEpochSecond(entry.getIntegratedTime())) - .orElseThrow( - () -> - new RekorVerificationException( - "Log entry (logid, timestamp) does not match any provided transparency logs.")); - - try { - var verifier = Verifiers.newVerifier(tlog.getPublicKey().toJavaPublicKey()); - if (!verifier.verify( - entry.getSignableContent(), - Base64.getDecoder().decode(entry.getVerification().getSignedEntryTimestamp()))) { - throw new RekorVerificationException("Entry SET was not valid"); - } - } catch (InvalidKeySpecException ike) { - throw new RekorVerificationException("Public Key could be parsed", ike); - } catch (InvalidKeyException ike) { - throw new RekorVerificationException("Public Key was invalid", ike); - } catch (SignatureException se) { - throw new RekorVerificationException("Signature was invalid", se); - } catch (NoSuchAlgorithmException nsae) { - throw new AssertionError("Required verification algorithm 'SHA256withECDSA' not found."); - } - } - - /** - * Verify that a Rekor Entry is in the log by checking inclusion proof. - * - * @param entry the entry to verify - * @throws RekorVerificationException if the entry cannot be verified - */ - public void verifyInclusionProof(RekorEntry entry) throws RekorVerificationException { - - var inclusionProof = - entry - .getVerification() - .getInclusionProof() - .orElseThrow( - () -> - new RekorVerificationException( - "No inclusion proof was found in the rekor entry")); - - var leafHash = - Hashing.sha256() - .hashBytes(combineBytes(new byte[] {0x00}, Base64.getDecoder().decode(entry.getBody()))) - .asBytes(); - - // see: https://datatracker.ietf.org/doc/rfc9162/ section 2.1.3.2 - - // nodeIndex and totalNodes represent values for a specific level in the tree - // starting at the leafs and moving up to the root. - var nodeIndex = inclusionProof.getLogIndex(); - var totalNodes = inclusionProof.getTreeSize() - 1; - - var currentHash = leafHash; - var hashes = inclusionProof.getHashes(); - - for (var hash : hashes) { - byte[] p = Hex.decode(hash); - if (totalNodes == 0) { - throw new RekorVerificationException("Inclusion proof failed, ended prematurely"); - } - if (nodeIndex == totalNodes || nodeIndex % 2 == 1) { - currentHash = hashChildren(p, currentHash); - while (nodeIndex % 2 == 0) { - nodeIndex = nodeIndex >> 1; - totalNodes = totalNodes >> 1; - } - } else { - currentHash = hashChildren(currentHash, p); - } - nodeIndex = nodeIndex >> 1; - totalNodes = totalNodes >> 1; - } - - var calcuatedRootHash = Hex.toHexString(currentHash); - if (!calcuatedRootHash.equals(inclusionProof.getRootHash())) { - throw new RekorVerificationException( - "Calculated inclusion proof root hash does not match provided root hash\n" - + calcuatedRootHash - + "\n" - + inclusionProof.getRootHash()); - } - } - - private static byte[] combineBytes(byte[] first, byte[] second) { - byte[] result = new byte[first.length + second.length]; - System.arraycopy(first, 0, result, 0, first.length); - System.arraycopy(second, 0, result, first.length, second.length); - return result; - } - - // hash the concatination of 0x01, left and right - private static byte[] hashChildren(byte[] left, byte[] right) { - return Hashing.sha256() - .hashBytes(combineBytes(new byte[] {0x01}, combineBytes(left, right))) - .asBytes(); - } -} diff --git a/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java b/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java index 5ac385fac..a7af7ff62 100644 --- a/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java +++ b/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java @@ -62,12 +62,12 @@ public static void setupArtifact() throws IOException { @SuppressWarnings("deprecation") @EnabledIfOidcExists(provider = OidcProviderType.ANY) public void sign_production() throws Exception { - var signer = KeylessSigner2.builder().sigstorePublicDefaults().build(); + var signer = KeylessSigner.builder().sigstorePublicDefaults().build(); var results = signer.sign(artifactDigests); verifySigningResult(results); - var verifier = KeylessVerifier2.builder().sigstorePublicDefaults().build(); + var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build(); for (var result : results) { verifier.verifyOnline( result.getDigest(), Certificates.toPemBytes(result.getCertPath()), result.getSignature()); @@ -79,11 +79,11 @@ public void sign_production() throws Exception { @SuppressWarnings("deprecation") @EnabledIfOidcExists(provider = OidcProviderType.ANY) public void sign_staging() throws Exception { - var signer = KeylessSigner2.builder().sigstoreStagingDefaults().build(); + var signer = KeylessSigner.builder().sigstoreStagingDefaults().build(); var results = signer.sign(artifactDigests); verifySigningResult(results); - var verifier = KeylessVerifier2.builder().sigstoreStagingDefaults().build(); + var verifier = KeylessVerifier.builder().sigstoreStagingDefaults().build(); for (var result : results) { verifier.verifyOnline( result.getDigest(), Certificates.toPemBytes(result.getCertPath()), result.getSignature()); diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClient2Test.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClient2Test.java deleted file mode 100644 index ecc92b164..000000000 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClient2Test.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2022 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.fulcio.client; - -import dev.sigstore.encryption.signers.Signers; -import dev.sigstore.http.ImmutableHttpParams; -import dev.sigstore.testing.FakeCTLogServer; -import dev.sigstore.testing.FulcioWrapper; -import dev.sigstore.testing.MockOAuth2ServerExtension; -import dev.sigstore.trustroot.CertificateAuthority; -import dev.sigstore.trustroot.ImmutableCertificateAuthority; -import dev.sigstore.trustroot.ImmutableValidFor; -import dev.sigstore.trustroot.Subject; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertPath; -import java.security.cert.CertificateException; -import java.time.Instant; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; - -public class FulcioClient2Test { - - @Test - @ExtendWith({FakeCTLogServer.class, MockOAuth2ServerExtension.class, FulcioWrapper.class}) - public void testSigningCert( - MockOAuth2ServerExtension mockOAuthServerExtension, FulcioWrapper fulcioWrapper) - throws Exception { - var c = - FulcioClient2.builder() - .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) - .setCertificateAuthority(createCA(fulcioWrapper.getGrpcURI2())) - .build(); - - // create a "subject" and sign it with the oidc server key (signed JWT) - var token = mockOAuthServerExtension.getOidcToken().getIdToken(); - var subject = mockOAuthServerExtension.getOidcToken().getSubjectAlternativeName(); - - var signer = Signers.newEcdsaSigner(); - var signed = signer.sign(subject.getBytes(StandardCharsets.UTF_8)); - - // create a certificate request with our public key and our signed "subject" - var cReq = CertificateRequest.newCertificateRequest(signer.getPublicKey(), token, signed); - - // ask fulcio for a signing cert - var sc = c.signingCertificate(cReq); - - // some pretty basic assertions - Assertions.assertTrue(sc.getCertPath().getCertificates().size() > 0); - Assertions.assertTrue(sc.hasEmbeddedSct()); - } - - @Test - @ExtendWith({MockOAuth2ServerExtension.class, FulcioWrapper.class}) - public void testSigningCert_NoSct( - MockOAuth2ServerExtension mockOAuthServerExtension, FulcioWrapper fulcioWrapper) - throws Exception { - var c = - FulcioClient2.builder() - .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) - .setCertificateAuthority(createCA(fulcioWrapper.getGrpcURI2())) - .build(); - - // create a "subject" and sign it with the oidc server key (signed JWT) - var token = mockOAuthServerExtension.getOidcToken().getIdToken(); - var subject = mockOAuthServerExtension.getOidcToken().getSubjectAlternativeName(); - - var signer = Signers.newRsaSigner(); - var signed = signer.sign(subject.getBytes(StandardCharsets.UTF_8)); - - // create a certificate request with our public key and our signed "subject" - var cReq = CertificateRequest.newCertificateRequest(signer.getPublicKey(), token, signed); - - // ask fulcio for a signing cert - var ex = Assertions.assertThrows(CertificateException.class, () -> c.signingCertificate(cReq)); - Assertions.assertEquals(ex.getMessage(), "Detached SCTs are not supported"); - } - - private CertificateAuthority createCA(URI uri) { - return ImmutableCertificateAuthority.builder() - .uri(uri) - .certPath(Mockito.mock(CertPath.class)) - .subject(Mockito.mock(Subject.class)) - .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) - .build(); - } -} diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java index 799d19252..12855f5ab 100644 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java @@ -20,11 +20,19 @@ import dev.sigstore.testing.FakeCTLogServer; import dev.sigstore.testing.FulcioWrapper; import dev.sigstore.testing.MockOAuth2ServerExtension; +import dev.sigstore.trustroot.CertificateAuthority; +import dev.sigstore.trustroot.ImmutableCertificateAuthority; +import dev.sigstore.trustroot.ImmutableValidFor; +import dev.sigstore.trustroot.Subject; +import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.cert.CertPath; +import java.security.cert.CertificateException; +import java.time.Instant; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; public class FulcioClientTest { @@ -36,7 +44,7 @@ public void testSigningCert( var c = FulcioClient.builder() .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) - .setServerUrl(fulcioWrapper.getGrpcURI()) + .setCertificateAuthority(createCA(fulcioWrapper.getGrpcURI2())) .build(); // create a "subject" and sign it with the oidc server key (signed JWT) @@ -54,7 +62,6 @@ public void testSigningCert( // some pretty basic assertions Assertions.assertTrue(sc.getCertPath().getCertificates().size() > 0); - Assertions.assertTrue(sc.getDetachedSct().isEmpty()); Assertions.assertTrue(sc.hasEmbeddedSct()); } @@ -66,8 +73,7 @@ public void testSigningCert_NoSct( var c = FulcioClient.builder() .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) - .setServerUrl(fulcioWrapper.getGrpcURI()) - .requireSct(false) + .setCertificateAuthority(createCA(fulcioWrapper.getGrpcURI2())) .build(); // create a "subject" and sign it with the oidc server key (signed JWT) @@ -81,15 +87,16 @@ public void testSigningCert_NoSct( var cReq = CertificateRequest.newCertificateRequest(signer.getPublicKey(), token, signed); // ask fulcio for a signing cert - var sc = c.signingCertificate(cReq); - - // some pretty basic assertions - Assertions.assertTrue(sc.getCertPath().getCertificates().size() > 0); - Assertions.assertFalse(sc.getDetachedSct().isPresent()); - Assertions.assertFalse(sc.hasEmbeddedSct()); + var ex = Assertions.assertThrows(CertificateException.class, () -> c.signingCertificate(cReq)); + Assertions.assertEquals(ex.getMessage(), "Detached SCTs are not supported"); } - @Test - @Disabled("TODO: until we can hit a fulcio instance that generates detached SCTs") - public void testSigningCert_detachedSCT() {} + private CertificateAuthority createCA(URI uri) { + return ImmutableCertificateAuthority.builder() + .uri(uri) + .certPath(Mockito.mock(CertPath.class)) + .subject(Mockito.mock(Subject.class)) + .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) + .build(); + } } diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifier2Test.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifier2Test.java deleted file mode 100644 index cd2c363f9..000000000 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifier2Test.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2022 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.fulcio.client; - -import com.google.common.io.Resources; -import com.google.protobuf.util.JsonFormat; -import dev.sigstore.encryption.certificates.transparency.SerializationException; -import dev.sigstore.proto.trustroot.v1.TrustedRoot; -import dev.sigstore.trustroot.ImmutableLogId; -import dev.sigstore.trustroot.ImmutableTransparencyLog; -import dev.sigstore.trustroot.ImmutableTransparencyLogs; -import dev.sigstore.trustroot.SigstoreTrustedRoot; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.spec.InvalidKeySpecException; -import java.util.Collections; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class FulcioVerifier2Test { - private static String sctBase64; - private static String certs; - private static String certs2; - private static byte[] fulcioRoot; - private static byte[] ctfePub; - private static byte[] badCtfePub; - private static String certsWithEmbeddedSct; - - private static SigstoreTrustedRoot trustRoot; - - @BeforeAll - public static void loadResources() throws IOException { - sctBase64 = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/sct.base64"), - StandardCharsets.UTF_8); - certs = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), - StandardCharsets.UTF_8); - - certs2 = - Resources.toString( - Resources.getResource("dev/sigstore/samples/certs/cert-githuboidc.pem"), - StandardCharsets.UTF_8); - - fulcioRoot = - Resources.toByteArray( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem")); - ctfePub = - Resources.toByteArray( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/ctfe.pub")); - badCtfePub = - Resources.toByteArray(Resources.getResource("dev/sigstore/samples/keys/test-rsa.pub")); - - certsWithEmbeddedSct = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/certWithSct.pem"), - StandardCharsets.UTF_8); - } - - @BeforeAll - public static void initTrustRoot() throws IOException, CertificateException { - var json = - Resources.toString( - Resources.getResource("dev/sigstore/trustroot/trusted_root.json"), - StandardCharsets.UTF_8); - var builder = TrustedRoot.newBuilder(); - JsonFormat.parser().merge(json, builder); - - trustRoot = SigstoreTrustedRoot.from(builder.build()); - } - - @Test - public void detachedSctNotSupported() throws Exception { - var fulcioVerifier = FulcioVerifier2.newFulcioVerifier(trustRoot); - - var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); - var ex = - Assertions.assertThrows( - FulcioVerificationException.class, () -> fulcioVerifier.verifySct(signingCertificate)); - Assertions.assertEquals( - ex.getMessage(), "Detached SCTs are not supported for validating certificates"); - } - - @Test - public void testVerifySct_nullCtLogKey() - throws IOException, SerializationException, CertificateException, InvalidKeySpecException, - NoSuchAlgorithmException, InvalidAlgorithmParameterException { - var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); - var fulcioVerifier = - FulcioVerifier2.newFulcioVerifier( - trustRoot.getCAs(), - ImmutableTransparencyLogs.builder() - .addAllTransparencyLogs(Collections.emptyList()) - .build()); - - try { - fulcioVerifier.verifySct(signingCertificate); - Assertions.fail(); - } catch (FulcioVerificationException fve) { - Assertions.assertEquals("No ct logs were provided to verifier", fve.getMessage()); - } - } - - @Test - public void testVerifySct_noSct() throws Exception { - var signingCertificate = SigningCertificate.newSigningCertificate(certs, null); - var fulcioVerifier = FulcioVerifier2.newFulcioVerifier(trustRoot); - - try { - fulcioVerifier.verifySct(signingCertificate); - Assertions.fail(); - } catch (FulcioVerificationException fve) { - Assertions.assertEquals("No valid SCTs were found during verification", fve.getMessage()); - } - } - - @Test - public void validSigningCertAndEmbeddedSct() throws Exception { - var signingCertificate = SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, null); - var fulcioVerifier = FulcioVerifier2.newFulcioVerifier(trustRoot); - - fulcioVerifier.verifyCertChain(signingCertificate); - fulcioVerifier.verifySct(signingCertificate); - } - - @Test - public void invalidEmbeddedSct() throws Exception { - var signingCertificate = SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, null); - var fulcioVerifier = - FulcioVerifier2.newFulcioVerifier( - trustRoot.getCAs(), - ImmutableTransparencyLogs.builder() - .addTransparencyLog( - ImmutableTransparencyLog.builder() - .from(trustRoot.getCTLogs().all().get(0)) - .logId( - ImmutableLogId.builder() - .keyId("abcd".getBytes(StandardCharsets.UTF_8)) - .build()) - .build()) - .build()); - - var fve = - Assertions.assertThrows( - FulcioVerificationException.class, () -> fulcioVerifier.verifySct(signingCertificate)); - Assertions.assertEquals("No valid SCTs were found, all(1) SCTs were invalid", fve.getMessage()); - } -} diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java index 8e09906ca..b535477ab 100644 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java @@ -16,14 +16,20 @@ package dev.sigstore.fulcio.client; import com.google.common.io.Resources; +import com.google.protobuf.util.JsonFormat; import dev.sigstore.encryption.certificates.transparency.SerializationException; +import dev.sigstore.proto.trustroot.v1.TrustedRoot; +import dev.sigstore.trustroot.ImmutableLogId; +import dev.sigstore.trustroot.ImmutableTransparencyLog; +import dev.sigstore.trustroot.ImmutableTransparencyLogs; +import dev.sigstore.trustroot.SigstoreTrustedRoot; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; -import java.util.List; +import java.util.Collections; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -31,11 +37,14 @@ public class FulcioVerifierTest { private static String sctBase64; private static String certs; + private static String certs2; private static byte[] fulcioRoot; private static byte[] ctfePub; private static byte[] badCtfePub; private static String certsWithEmbeddedSct; + private static SigstoreTrustedRoot trustRoot; + @BeforeAll public static void loadResources() throws IOException { sctBase64 = @@ -47,6 +56,11 @@ public static void loadResources() throws IOException { Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), StandardCharsets.UTF_8); + certs2 = + Resources.toString( + Resources.getResource("dev/sigstore/samples/certs/cert-githuboidc.pem"), + StandardCharsets.UTF_8); + fulcioRoot = Resources.toByteArray( Resources.getResource("dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem")); @@ -62,16 +76,28 @@ public static void loadResources() throws IOException { StandardCharsets.UTF_8); } + @BeforeAll + public static void initTrustRoot() throws IOException, CertificateException { + var json = + Resources.toString( + Resources.getResource("dev/sigstore/trustroot/trusted_root.json"), + StandardCharsets.UTF_8); + var builder = TrustedRoot.newBuilder(); + JsonFormat.parser().merge(json, builder); + + trustRoot = SigstoreTrustedRoot.from(builder.build()); + } + @Test - public void validSigningCertAndDetachedSct() - throws IOException, SerializationException, CertificateException, InvalidKeySpecException, - NoSuchAlgorithmException, InvalidAlgorithmParameterException, - FulcioVerificationException { - var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(fulcioRoot, List.of(ctfePub, badCtfePub)); + public void detachedSctNotSupported() throws Exception { + var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); - fulcioVerifier.verifyCertChain(signingCertificate); - fulcioVerifier.verifySct(signingCertificate); + var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); + var ex = + Assertions.assertThrows( + FulcioVerificationException.class, () -> fulcioVerifier.verifySct(signingCertificate)); + Assertions.assertEquals( + ex.getMessage(), "Detached SCTs are not supported for validating certificates"); } @Test @@ -79,70 +105,63 @@ public void testVerifySct_nullCtLogKey() throws IOException, SerializationException, CertificateException, InvalidKeySpecException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(fulcioRoot, null); + var fulcioVerifier = + FulcioVerifier.newFulcioVerifier( + trustRoot.getCAs(), + ImmutableTransparencyLogs.builder() + .addAllTransparencyLogs(Collections.emptyList()) + .build()); try { fulcioVerifier.verifySct(signingCertificate); Assertions.fail(); } catch (FulcioVerificationException fve) { - Assertions.assertEquals("No ct-log public key was provided to verifier", fve.getMessage()); + Assertions.assertEquals("No ct logs were provided to verifier", fve.getMessage()); } } @Test - public void testVerifySct_noSct() - throws SerializationException, CertificateException, IOException, - InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchAlgorithmException { + public void testVerifySct_noSct() throws Exception { var signingCertificate = SigningCertificate.newSigningCertificate(certs, null); - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(fulcioRoot, List.of(ctfePub, badCtfePub)); + var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); try { fulcioVerifier.verifySct(signingCertificate); Assertions.fail(); } catch (FulcioVerificationException fve) { - Assertions.assertEquals( - "No detached or embedded SCTs were found to verify", fve.getMessage()); + Assertions.assertEquals("No valid SCTs were found during verification", fve.getMessage()); } } @Test - public void validSigningCertAndEmbeddedSct() - throws IOException, SerializationException, CertificateException, InvalidKeySpecException, - NoSuchAlgorithmException, InvalidAlgorithmParameterException, - FulcioVerificationException { + public void validSigningCertAndEmbeddedSct() throws Exception { var signingCertificate = SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, null); - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(fulcioRoot, List.of(ctfePub, badCtfePub)); + var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); fulcioVerifier.verifyCertChain(signingCertificate); fulcioVerifier.verifySct(signingCertificate); } @Test - public void invalidEmbeddedSct() - throws SerializationException, CertificateException, IOException, - InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchAlgorithmException, - FulcioVerificationException { + public void invalidEmbeddedSct() throws Exception { var signingCertificate = SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, null); - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(fulcioRoot, List.of(badCtfePub)); - - var fve = - Assertions.assertThrows( - FulcioVerificationException.class, () -> fulcioVerifier.verifySct(signingCertificate)); - Assertions.assertEquals( - "Expecting at least one valid sct, but found 0 valid and 1 invalid scts", fve.getMessage()); - } - - @Test - public void invalidDetachedSct() - throws SerializationException, CertificateException, IOException, - InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchAlgorithmException { - var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(fulcioRoot, List.of(badCtfePub)); + var fulcioVerifier = + FulcioVerifier.newFulcioVerifier( + trustRoot.getCAs(), + ImmutableTransparencyLogs.builder() + .addTransparencyLog( + ImmutableTransparencyLog.builder() + .from(trustRoot.getCTLogs().all().get(0)) + .logId( + ImmutableLogId.builder() + .keyId("abcd".getBytes(StandardCharsets.UTF_8)) + .build()) + .build()) + .build()); var fve = Assertions.assertThrows( FulcioVerificationException.class, () -> fulcioVerifier.verifySct(signingCertificate)); - // TODO: this error message could probably use some work - Assertions.assertEquals("SCT could not be verified because UNKNOWN_LOG", fve.getMessage()); + Assertions.assertEquals("No valid SCTs were found, all(1) SCTs were invalid", fve.getMessage()); } } diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClient2Test.java b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClient2Test.java deleted file mode 100644 index b2d5e9ad7..000000000 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClient2Test.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2022 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.rekor.client; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.common.collect.ImmutableList; -import dev.sigstore.encryption.certificates.Certificates; -import dev.sigstore.encryption.signers.Signers; -import dev.sigstore.testing.CertGenerator; -import dev.sigstore.trustroot.ImmutableTransparencyLog; -import dev.sigstore.trustroot.LogId; -import dev.sigstore.trustroot.PublicKey; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.cert.CertificateException; -import java.util.Optional; -import java.util.UUID; -import org.bouncycastle.operator.OperatorCreationException; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -public class RekorClient2Test { - - private static final String REKOR_URL = "https://rekor.sigstage.dev"; - private RekorClient2 client; - - @BeforeEach - public void setupClient() throws URISyntaxException { - // this tests directly against rekor in staging, it's a bit hard to bring up a rekor instance - // without docker compose. - client = - RekorClient2.builder() - .setTransparencyLog( - ImmutableTransparencyLog.builder() - .baseUrl(URI.create(REKOR_URL)) - .hashAlgorithm("ignored") - .publicKey(Mockito.mock(PublicKey.class)) - .logId(Mockito.mock(LogId.class)) - .build()) - .build(); - } - - @Test - public void putEntry_toStaging() throws Exception { - HashedRekordRequest req = createdRekorRequest(); - var resp = client.putEntry(req); - - // pretty basic testing - MatcherAssert.assertThat( - resp.getEntryLocation().toString(), - CoreMatchers.startsWith(REKOR_URL + "/api/v1/log/entries/")); - - assertNotNull(resp.getUuid()); - assertNotNull(resp.getRaw()); - var entry = resp.getEntry(); - assertNotNull(entry.getBody()); - Assertions.assertTrue(entry.getIntegratedTime() > 1); - assertNotNull(entry.getLogID()); - Assertions.assertTrue(entry.getLogIndex() > 0); - assertNotNull(entry.getVerification().getSignedEntryTimestamp()); - // Assertions.assertNotNull(entry.getVerification().getInclusionProof()); - } - - // TODO(patrick@chainguard.dev): don't use data from prod, create the data as part of the test - // setup in staging. - @Test - public void searchEntries_nullParams() throws IOException { - assertEquals(ImmutableList.of(), client.searchEntry(null, null, null, null)); - } - - @Test - public void searchEntries_oneResult_hash() throws Exception { - var newRekordRequest = createdRekorRequest(); - client.putEntry(newRekordRequest); - assertEquals( - 1, - client - .searchEntry( - null, newRekordRequest.getHashedRekord().getData().getHash().getValue(), null, null) - .size()); - } - - @Test - public void searchEntries_oneResult_publicKey() throws Exception { - var newRekordRequest = createdRekorRequest(); - var resp = client.putEntry(newRekordRequest); - assertEquals( - 1, - client - .searchEntry( - null, - null, - "x509", - RekorTypes.getHashedRekord(resp.getEntry()) - .getSignature() - .getPublicKey() - .getContent()) - .size()); - } - - @Test - public void searchEntries_moreThanOneResult_email() throws Exception { - var newRekordRequest = createdRekorRequest(); - var newRekordRequest2 = createdRekorRequest(); - client.putEntry(newRekordRequest); - client.putEntry(newRekordRequest2); - assertTrue( - client.searchEntry("test@test.com", null, null, null).size() - > 1); // as long as our tests use staging this is just going to grow. - } - - @Test - public void searchEntries_zeroResults() throws IOException { - assertTrue( - client - .searchEntry( - null, - "sha256:9f54fad117567ab4c2c6738beef765f7c362550534ffc0bfe8d96b0236d69661", // made - // up sha - null, - null) - .isEmpty()); - } - - @Test - public void getEntry_entryExists() throws Exception { - var newRekordRequest = createdRekorRequest(); - var resp = client.putEntry(newRekordRequest); - var entry = client.getEntry(resp.getUuid()); - assertEntry(resp, entry); - } - - @Test - public void getEntry_hashedRekordRequest_byCalculatedUuid() throws Exception { - var hashedRekordRequest = createdRekorRequest(); - var resp = client.putEntry(hashedRekordRequest); - // getting an entry by hashedrekordrequest should implicitly calculate uuid - // from the contents of the hashedrekord - var entry = client.getEntry(hashedRekordRequest); - assertEntry(resp, entry); - } - - private void assertEntry(RekorResponse resp, Optional entry) { - assertTrue(entry.isPresent()); - assertEquals(resp.getEntry().getLogID(), entry.get().getLogID()); - assertTrue(entry.get().getVerification().getInclusionProof().isPresent()); - assertNotNull(entry.get().getVerification().getInclusionProof().get().getTreeSize()); - assertNotNull(entry.get().getVerification().getInclusionProof().get().getRootHash()); - assertNotNull(entry.get().getVerification().getInclusionProof().get().getLogIndex()); - assertTrue(entry.get().getVerification().getInclusionProof().get().getHashes().size() > 0); - } - - @Test - public void getEntry_entryDoesntExist() throws Exception { - Optional entry = - client.getEntry( - "a8d2b213aa7efc1b2c9ccfa2fa647d00b34c63972e04e90276b5c31e0f317afd"); // I made this up - assertTrue(entry.isEmpty()); - } - - @NotNull - private HashedRekordRequest createdRekorRequest() - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, - OperatorCreationException, CertificateException, IOException { - // the data we want to sign - var data = "some data " + UUID.randomUUID(); - - // get the digest - var artifactDigest = - MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8)); - - // sign the full content (these signers do the artifact hashing themselves) - var signer = Signers.newEcdsaSigner(); - var signature = signer.sign(data.getBytes(StandardCharsets.UTF_8)); - - // create a fake signing cert (not fulcio/dex) - var cert = Certificates.toPemBytes(CertGenerator.newCert(signer.getPublicKey())); - - return HashedRekordRequest.newHashedRekordRequest(artifactDigest, cert, signature); - } -} diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientTest.java b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientTest.java index 5ba6fb7cf..60d9f445c 100644 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorClientTest.java @@ -15,12 +15,17 @@ */ package dev.sigstore.rekor.client; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.collect.ImmutableList; import dev.sigstore.encryption.certificates.Certificates; import dev.sigstore.encryption.signers.Signers; import dev.sigstore.testing.CertGenerator; +import dev.sigstore.trustroot.ImmutableTransparencyLog; +import dev.sigstore.trustroot.LogId; +import dev.sigstore.trustroot.PublicKey; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -39,6 +44,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; public class RekorClientTest { @@ -49,7 +55,16 @@ public class RekorClientTest { public void setupClient() throws URISyntaxException { // this tests directly against rekor in staging, it's a bit hard to bring up a rekor instance // without docker compose. - client = RekorClient.builder().setServerUrl(new URI(REKOR_URL)).build(); + client = + RekorClient.builder() + .setTransparencyLog( + ImmutableTransparencyLog.builder() + .baseUrl(URI.create(REKOR_URL)) + .hashAlgorithm("ignored") + .publicKey(Mockito.mock(PublicKey.class)) + .logId(Mockito.mock(LogId.class)) + .build()) + .build(); } @Test diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifier2Test.java b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifier2Test.java deleted file mode 100644 index 0698d301c..000000000 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifier2Test.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2022 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.rekor.client; - -import com.google.common.io.Resources; -import com.google.protobuf.util.JsonFormat; -import dev.sigstore.proto.trustroot.v1.TrustedRoot; -import dev.sigstore.trustroot.ImmutableLogId; -import dev.sigstore.trustroot.ImmutablePublicKey; -import dev.sigstore.trustroot.ImmutableTransparencyLog; -import dev.sigstore.trustroot.ImmutableTransparencyLogs; -import dev.sigstore.trustroot.ImmutableValidFor; -import dev.sigstore.trustroot.SigstoreTrustedRoot; -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import org.bouncycastle.util.encoders.Base64; -import org.bouncycastle.util.encoders.Hex; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class RekorVerifier2Test { - public String rekorResponse; - public String rekorQueryResponse; - public byte[] rekorPub; - - public static SigstoreTrustedRoot trustRoot; - - @BeforeEach - public void loadResources() throws IOException { - rekorResponse = - Resources.toString( - Resources.getResource("dev/sigstore/samples/rekor-response/valid/response.json"), - StandardCharsets.UTF_8); - rekorPub = - Resources.toByteArray( - Resources.getResource("dev/sigstore/samples/rekor-response/valid/rekor.pub")); - rekorQueryResponse = - Resources.toString( - Resources.getResource("dev/sigstore/samples/rekor-response/valid/query-response.json"), - StandardCharsets.UTF_8); - } - - @BeforeAll - public static void initTrustRoot() throws IOException, CertificateException { - var json = - Resources.toString( - Resources.getResource("dev/sigstore/trustroot/staging_trusted_root.json"), - StandardCharsets.UTF_8); - var builder = TrustedRoot.newBuilder(); - JsonFormat.parser().merge(json, builder); - - trustRoot = SigstoreTrustedRoot.from(builder.build()); - } - - @Test - public void verifyEntry_valid() throws Exception { - var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse); - var verifier = RekorVerifier2.newRekorVerifier(trustRoot); - - verifier.verifyEntry(response.getEntry()); - } - - @Test - public void verifyEntry_invalid() throws Exception { - // change the logindex - var invalidResponse = rekorResponse.replace("79", "80"); - var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), invalidResponse); - var verifier = RekorVerifier2.newRekorVerifier(trustRoot); - - var thrown = - Assertions.assertThrows( - RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry())); - Assertions.assertEquals("Entry SET was not valid", thrown.getMessage()); - } - - @Test - public void verifyEntry_withInclusionProof() throws Exception { - var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorQueryResponse); - var verifier = RekorVerifier2.newRekorVerifier(trustRoot); - - var entry = response.getEntry(); - verifier.verifyEntry(entry); - verifier.verifyInclusionProof(entry); - } - - @Test - public void verifyEntry_withInvalidInclusionProof() throws Exception { - // replace a hash in the inclusion proof to make it bad - var invalidResponse = rekorQueryResponse.replace("b4439e", "aaaaaa"); - - var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), invalidResponse); - var verifier = RekorVerifier2.newRekorVerifier(trustRoot); - - var entry = response.getEntry(); - verifier.verifyEntry(entry); - - var thrown = - Assertions.assertThrows( - RekorVerificationException.class, () -> verifier.verifyInclusionProof(entry)); - MatcherAssert.assertThat( - thrown.getMessage(), - CoreMatchers.startsWith( - "Calculated inclusion proof root hash does not match provided root hash")); - } - - @Test - public void verifyEntry_logIdMismatch() throws Exception { - var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse); - var tlog = - ImmutableTransparencyLog.builder() - .logId( - ImmutableLogId.builder().keyId("garbage".getBytes(StandardCharsets.UTF_8)).build()) - .publicKey( - ImmutablePublicKey.builder() - .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) - .keyDetails("PKIX_ECDSA_P256_SHA_256") - .rawBytes( - Base64.decode( - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==")) - .build()) - .hashAlgorithm("ignored") - .baseUrl(URI.create("ignored")) - .build(); - - var verifier = - RekorVerifier2.newRekorVerifier( - ImmutableTransparencyLogs.builder().addTransparencyLog(tlog).build()); - - // make sure the entry time is valid for the log -- so we can determine the logid is the error - // creator - Assertions.assertTrue( - tlog.getPublicKey() - .getValidFor() - .contains(Instant.ofEpochSecond(response.getEntry().getIntegratedTime()))); - - var thrown = - Assertions.assertThrows( - RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry())); - Assertions.assertEquals( - "Log entry (logid, timestamp) does not match any provided transparency logs.", - thrown.getMessage()); - } - - @Test - public void verifyEntry_logIdTimeMismatch() throws Exception { - - var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse); - - var tlog = - ImmutableTransparencyLog.builder() - .logId( - ImmutableLogId.builder() - .keyId(Base64.decode("0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=")) - .build()) - .publicKey( - ImmutablePublicKey.builder() - .validFor( - ImmutableValidFor.builder() - .start(Instant.EPOCH) - .end(Instant.EPOCH.plus(1, ChronoUnit.SECONDS)) - .build()) - .keyDetails("PKIX_ECDSA_P256_SHA_256") - .rawBytes( - Base64.decode( - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==")) - .build()) - .hashAlgorithm("ignored") - .baseUrl(URI.create("ignored")) - .build(); - - var verifier = - RekorVerifier2.newRekorVerifier( - ImmutableTransparencyLogs.builder().addTransparencyLog(tlog).build()); - - // make sure logId is equal -- so we can determine the time is the error creator - Assertions.assertArrayEquals( - tlog.getLogId().getKeyId(), Hex.decode(response.getEntry().getLogID())); - - var thrown = - Assertions.assertThrows( - RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry())); - Assertions.assertEquals( - "Log entry (logid, timestamp) does not match any provided transparency logs.", - thrown.getMessage()); - } -} diff --git a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifierTest.java index 7a2dda945..a37e7c251 100644 --- a/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifierTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/rekor/client/RekorVerifierTest.java @@ -16,12 +16,26 @@ package dev.sigstore.rekor.client; import com.google.common.io.Resources; +import com.google.protobuf.util.JsonFormat; +import dev.sigstore.proto.trustroot.v1.TrustedRoot; +import dev.sigstore.trustroot.ImmutableLogId; +import dev.sigstore.trustroot.ImmutablePublicKey; +import dev.sigstore.trustroot.ImmutableTransparencyLog; +import dev.sigstore.trustroot.ImmutableTransparencyLogs; +import dev.sigstore.trustroot.ImmutableValidFor; +import dev.sigstore.trustroot.SigstoreTrustedRoot; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,6 +44,8 @@ public class RekorVerifierTest { public String rekorQueryResponse; public byte[] rekorPub; + public static SigstoreTrustedRoot trustRoot; + @BeforeEach public void loadResources() throws IOException { rekorResponse = @@ -45,10 +61,22 @@ public void loadResources() throws IOException { StandardCharsets.UTF_8); } + @BeforeAll + public static void initTrustRoot() throws IOException, CertificateException { + var json = + Resources.toString( + Resources.getResource("dev/sigstore/trustroot/staging_trusted_root.json"), + StandardCharsets.UTF_8); + var builder = TrustedRoot.newBuilder(); + JsonFormat.parser().merge(json, builder); + + trustRoot = SigstoreTrustedRoot.from(builder.build()); + } + @Test public void verifyEntry_valid() throws Exception { var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse); - var verifier = RekorVerifier.newRekorVerifier(rekorPub); + var verifier = RekorVerifier.newRekorVerifier(trustRoot); verifier.verifyEntry(response.getEntry()); } @@ -58,7 +86,7 @@ public void verifyEntry_invalid() throws Exception { // change the logindex var invalidResponse = rekorResponse.replace("79", "80"); var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), invalidResponse); - var verifier = RekorVerifier.newRekorVerifier(rekorPub); + var verifier = RekorVerifier.newRekorVerifier(trustRoot); var thrown = Assertions.assertThrows( @@ -69,7 +97,7 @@ public void verifyEntry_invalid() throws Exception { @Test public void verifyEntry_withInclusionProof() throws Exception { var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorQueryResponse); - var verifier = RekorVerifier.newRekorVerifier(rekorPub); + var verifier = RekorVerifier.newRekorVerifier(trustRoot); var entry = response.getEntry(); verifier.verifyEntry(entry); @@ -82,7 +110,7 @@ public void verifyEntry_withInvalidInclusionProof() throws Exception { var invalidResponse = rekorQueryResponse.replace("b4439e", "aaaaaa"); var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), invalidResponse); - var verifier = RekorVerifier.newRekorVerifier(rekorPub); + var verifier = RekorVerifier.newRekorVerifier(trustRoot); var entry = response.getEntry(); verifier.verifyEntry(entry); @@ -98,15 +126,82 @@ public void verifyEntry_withInvalidInclusionProof() throws Exception { @Test public void verifyEntry_logIdMismatch() throws Exception { - var garbageKey = - Resources.toByteArray(Resources.getResource("dev/sigstore/samples/keys/test-rsa.pub")); + var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse); + var tlog = + ImmutableTransparencyLog.builder() + .logId( + ImmutableLogId.builder().keyId("garbage".getBytes(StandardCharsets.UTF_8)).build()) + .publicKey( + ImmutablePublicKey.builder() + .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) + .keyDetails("PKIX_ECDSA_P256_SHA_256") + .rawBytes( + Base64.decode( + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==")) + .build()) + .hashAlgorithm("ignored") + .baseUrl(URI.create("ignored")) + .build(); + + var verifier = + RekorVerifier.newRekorVerifier( + ImmutableTransparencyLogs.builder().addTransparencyLog(tlog).build()); + + // make sure the entry time is valid for the log -- so we can determine the logid is the error + // creator + Assertions.assertTrue( + tlog.getPublicKey() + .getValidFor() + .contains(Instant.ofEpochSecond(response.getEntry().getIntegratedTime()))); + + var thrown = + Assertions.assertThrows( + RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry())); + Assertions.assertEquals( + "Log entry (logid, timestamp) does not match any provided transparency logs.", + thrown.getMessage()); + } + + @Test + public void verifyEntry_logIdTimeMismatch() throws Exception { var response = RekorResponse.newRekorResponse(new URI("https://somewhere"), rekorResponse); - var verifier = RekorVerifier.newRekorVerifier(garbageKey); + + var tlog = + ImmutableTransparencyLog.builder() + .logId( + ImmutableLogId.builder() + .keyId(Base64.decode("0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=")) + .build()) + .publicKey( + ImmutablePublicKey.builder() + .validFor( + ImmutableValidFor.builder() + .start(Instant.EPOCH) + .end(Instant.EPOCH.plus(1, ChronoUnit.SECONDS)) + .build()) + .keyDetails("PKIX_ECDSA_P256_SHA_256") + .rawBytes( + Base64.decode( + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==")) + .build()) + .hashAlgorithm("ignored") + .baseUrl(URI.create("ignored")) + .build(); + + var verifier = + RekorVerifier.newRekorVerifier( + ImmutableTransparencyLogs.builder().addTransparencyLog(tlog).build()); + + // make sure logId is equal -- so we can determine the time is the error creator + Assertions.assertArrayEquals( + tlog.getLogId().getKeyId(), Hex.decode(response.getEntry().getLogID())); var thrown = Assertions.assertThrows( RekorVerificationException.class, () -> verifier.verifyEntry(response.getEntry())); - Assertions.assertEquals("LogId does not match supplied rekor public key.", thrown.getMessage()); + Assertions.assertEquals( + "Log entry (logid, timestamp) does not match any provided transparency logs.", + thrown.getMessage()); } } diff --git a/sigstore-java/src/test/java/dev/sigstore/trustroot/CertificateAuthoritiesTest.java b/sigstore-java/src/test/java/dev/sigstore/trustroot/CertificateAuthoritiesTest.java index fe449a3ae..e0a1c4c7d 100644 --- a/sigstore-java/src/test/java/dev/sigstore/trustroot/CertificateAuthoritiesTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/trustroot/CertificateAuthoritiesTest.java @@ -15,8 +15,6 @@ */ package dev.sigstore.trustroot; -import static org.junit.jupiter.api.Assertions.*; - import java.net.URI; import java.security.cert.CertPath; import java.time.Instant;