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);
+ }
+}