From 93692c10ad493517d47f24005f8c552d77c62580 Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Wed, 6 Sep 2023 14:48:35 -0400 Subject: [PATCH] do not merge --- .../java/dev/sigstore/KeylessSigner2.java | 406 ++++++++++++++++++ .../java/dev/sigstore/KeylessVerifier2.java | 296 +++++++++++++ .../test/java/dev/sigstore/Keyless2Test.java | 127 ++++++ 3 files changed, 829 insertions(+) create mode 100644 sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java create mode 100644 sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java create mode 100644 sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java new file mode 100644 index 00000000..1b2804a2 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner2.java @@ -0,0 +1,406 @@ +/* + * 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.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.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.OidcClient; +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.RekorClient; +import dev.sigstore.rekor.client.RekorParseException; +import dev.sigstore.rekor.client.RekorVerificationException; +import dev.sigstore.rekor.client.RekorVerifier; +import java.io.IOException; +import java.net.URI; +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 FulcioClient fulcioClient; + private final FulcioVerifier fulcioVerifier; + private final RekorClient rekorClient; + private final RekorVerifier 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( + FulcioClient fulcioClient, + FulcioVerifier fulcioVerifier, + RekorClient rekorClient, + RekorVerifier 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 FulcioClient fulcioClient; + private FulcioVerifier fulcioVerifier; + private RekorClient rekorClient; + private RekorVerifier rekorVerifier; + 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; + 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; + 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() { + Preconditions.checkNotNull(fulcioClient, "fulcioClient"); + Preconditions.checkNotNull(fulcioVerifier, "fulcioVerifier"); + Preconditions.checkNotNull(rekorClient, "rekorClient"); + Preconditions.checkNotNull(rekorVerifier, "rekorVerifier"); + Preconditions.checkNotNull(oidcClients, "oidcClients"); + Preconditions.checkNotNull(signer, "signer"); + return new KeylessSigner2( + fulcioClient, + fulcioVerifier, + rekorClient, + rekorVerifier, + oidcClients, + signer, + minSigningCertificateLifetime); + } + + @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())); + oidcClients(OidcClients.DEFAULTS); + signer(Signers.newEcdsaSigner()); + minSigningCertificateLifetime(DEFAULT_MIN_SIGNING_CERTIFICATE_LIFETIME); + return this; + } + + @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())); + 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 { + + 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 { + 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 { + 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 { + return signFiles(List.of(artifact)).get(artifact); + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java new file mode 100644 index 00000000..92fc391e --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier2.java @@ -0,0 +1,296 @@ +/* + * 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.FulcioVerifier; +import dev.sigstore.fulcio.client.SigningCertificate; +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 java.io.IOException; +import java.net.URI; +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 FulcioVerifier fulcioVerifier; + private final RekorVerifier rekorVerifier; + private final RekorClient rekorClient; + + private KeylessVerifier2( + FulcioVerifier fulcioVerifier, RekorClient rekorClient, RekorVerifier rekorVerifier) { + this.fulcioVerifier = fulcioVerifier; + this.rekorClient = rekorClient; + this.rekorVerifier = rekorVerifier; + } + + public static KeylessVerifier2.Builder builder() { + return new KeylessVerifier2.Builder(); + } + + public static class Builder { + private FulcioVerifier fulcioVerifier; + private RekorClient rekorClient; + private RekorVerifier rekorVerifier; + + public KeylessVerifier2.Builder fulcioVerifier(FulcioVerifier fulcioVerifier) { + this.fulcioVerifier = fulcioVerifier; + return this; + } + + public KeylessVerifier2.Builder rekorClient( + RekorClient rekorClient, RekorVerifier rekorVerifier) { + this.rekorClient = rekorClient; + this.rekorVerifier = rekorVerifier; + return this; + } + + public KeylessVerifier2 build() { + Preconditions.checkNotNull(fulcioVerifier); + Preconditions.checkNotNull(rekorVerifier); + Preconditions.checkNotNull(rekorClient); + return new KeylessVerifier2(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())); + 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())); + 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 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/test/java/dev/sigstore/Keyless2Test.java b/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java new file mode 100644 index 00000000..a7af7ff6 --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java @@ -0,0 +1,127 @@ +/* + * 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.common.hash.Hashing; +import dev.sigstore.bundle.BundleFactory; +import dev.sigstore.encryption.certificates.Certificates; +import dev.sigstore.rekor.client.RekorTypeException; +import dev.sigstore.rekor.client.RekorTypes; +import dev.sigstore.testkit.annotations.EnabledIfOidcExists; +import dev.sigstore.testkit.annotations.OidcProviderType; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class Keyless2Test { + @TempDir public static Path testRoot; + + public static List artifactDigests; + + @BeforeAll + public static void setupArtifact() throws IOException { + artifactDigests = new ArrayList<>(); + + for (int i = 0; i < 2; i++) { + var artifact = testRoot.resolve("artifact" + i + ".e2e"); + Files.createFile(artifact); + Files.write( + artifact, ("some test data " + UUID.randomUUID()).getBytes(StandardCharsets.UTF_8)); + var digest = + com.google.common.io.Files.asByteSource(artifact.toFile()) + .hash(Hashing.sha256()) + .asBytes(); + artifactDigests.add(digest); + } + } + + @Test + @SuppressWarnings("deprecation") + @EnabledIfOidcExists(provider = OidcProviderType.ANY) + public void sign_production() throws Exception { + var signer = KeylessSigner.builder().sigstorePublicDefaults().build(); + var results = signer.sign(artifactDigests); + + verifySigningResult(results); + + var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build(); + for (var result : results) { + verifier.verifyOnline( + result.getDigest(), Certificates.toPemBytes(result.getCertPath()), result.getSignature()); + checkBundleSerialization(result); + } + } + + @Test + @SuppressWarnings("deprecation") + @EnabledIfOidcExists(provider = OidcProviderType.ANY) + public void sign_staging() throws Exception { + var signer = KeylessSigner.builder().sigstoreStagingDefaults().build(); + var results = signer.sign(artifactDigests); + verifySigningResult(results); + + var verifier = KeylessVerifier.builder().sigstoreStagingDefaults().build(); + for (var result : results) { + verifier.verifyOnline( + result.getDigest(), Certificates.toPemBytes(result.getCertPath()), result.getSignature()); + checkBundleSerialization(result); + } + } + + private void verifySigningResult(List results) + throws IOException, RekorTypeException { + + Assertions.assertEquals(artifactDigests.size(), results.size()); + + for (int i = 0; i < results.size(); i++) { + var result = results.get(i); + var artifactDigest = artifactDigests.get(i); + Assertions.assertNotNull(result.getDigest()); + Assertions.assertNotNull(result.getCertPath()); + Assertions.assertNotNull(result.getEntry()); + Assertions.assertNotNull(result.getSignature()); + + var hr = RekorTypes.getHashedRekord(result.getEntry().get()); + // check if ht rekor entry has the digest we sent + Assertions.assertArrayEquals(artifactDigest, result.getDigest()); + // check if the rekor entry has the signature we sent + Assertions.assertArrayEquals( + Base64.getDecoder().decode(hr.getSignature().getContent()), result.getSignature()); + // check if the rekor entry has the certificate we sent + Assertions.assertArrayEquals( + Base64.getDecoder().decode(hr.getSignature().getPublicKey().getContent()), + Certificates.toPemBytes(result.getCertPath().getCertificates().get(0))); + } + } + + private void checkBundleSerialization(KeylessSignature keylessSignature) throws Exception { + var bundleJson = BundleFactory.createBundle(keylessSignature); + var keylessSignatureFromBundle = BundleFactory.readBundle(new StringReader(bundleJson)); + var bundleJson2 = BundleFactory.createBundle(keylessSignatureFromBundle); + Assertions.assertEquals(bundleJson, bundleJson2); + Assertions.assertEquals(keylessSignature, keylessSignatureFromBundle); + } +}