Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 60 additions & 32 deletions sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,31 +132,30 @@ public Builder trustedRootProvider(TrustedRootProvider trustedRootProvider) {
private AlgorithmRegistry.HashAlgorithm findHashAlgorithm(Bundle bundle)
throws KeylessVerificationException {
try {
var rekorEntry = bundle.getEntries().get(0);
var body = rekorEntry.getBodyDecoded();
if ("0.0.1".equals(body.getApiVersion())) {
if ("hashedrekord".equals(body.getKind())) {
var entry = RekorTypes.getHashedRekordV001(rekorEntry);
switch (entry.getData().getHash().getAlgorithm()) {
case SHA_256:
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
case SHA_384:
return AlgorithmRegistry.HashAlgorithm.SHA2_384;
case SHA_512:
return AlgorithmRegistry.HashAlgorithm.SHA2_512;
// special case hashedrekord:0.0.1 and dsse:0.0.1 which may contain legacy public key to hash
// algorithm mappings
if (!bundle.getEntries().isEmpty()) {
var rekorEntry = bundle.getEntries().get(0);
var body = rekorEntry.getBodyDecoded();
if ("0.0.1".equals(body.getApiVersion())) {
if ("hashedrekord".equals(body.getKind())) {
var entry = RekorTypes.getHashedRekordV001(rekorEntry);
switch (entry.getData().getHash().getAlgorithm()) {
case SHA_256:
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
case SHA_384:
return AlgorithmRegistry.HashAlgorithm.SHA2_384;
case SHA_512:
return AlgorithmRegistry.HashAlgorithm.SHA2_512;
}
} else if ("dsse".equals(body.getKind())) {
// dsse:0.0.1 is always sha256
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
}
} else if ("dsse".equals(body.getKind())) {
// dsse:0.0.1 is always sha256
return AlgorithmRegistry.HashAlgorithm.SHA2_256;
}
} else if ("0.0.2".equals(body.getApiVersion())) {
// rekor v2 entries must conform to the Algorithm Registry
var publicKey = Certificates.getLeaf(bundle.getCertPath()).getPublicKey();
var signingAlgorithm = AlgorithmRegistry.getSigningAlgorithm(publicKey);
return signingAlgorithm.getHashAlgorithm();
}
throw new KeylessVerificationException(
"Unsupported entry type: '" + body.getKind() + ":" + body.getApiVersion() + "'");
var publicKey = Certificates.getLeaf(bundle.getCertPath()).getPublicKey();
return AlgorithmRegistry.getSigningAlgorithm(publicKey).getHashAlgorithm();
} catch (JsonParseException | RekorTypeException | UnsupportedAlgorithmException ex) {
throw new KeylessVerificationException(
"Could not determine hash algorithm from sigstore bundle", ex);
Expand Down Expand Up @@ -199,7 +198,7 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt

// a trusted CT log
try {
fulcioVerifier.verifySigningCertificate(signingCert);
fulcioVerifier.verifySigningCertificate(signingCert, options.getCtLogOptions());
} catch (FulcioVerificationException | IOException ex) {
throw new KeylessVerificationException(
"Fulcio certificate was not valid: " + ex.getMessage(), ex);
Expand All @@ -209,7 +208,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
checkCertificateMatchers(leafCert, options.getCertificateMatchers());

var hashAlgorithm = findHashAlgorithm(bundle);
var rekorEntry = bundle.getEntries().get(0);

// A transparency-log entry is required unless disabled (e.g. a private deployment that verifies
// via a signed timestamp instead). We also refuse to ignore an entry that is present, so a
// disabled policy cannot silently skip verifying a log entry the bundle actually contains.
var entries = bundle.getEntries();
if (entries.isEmpty() && options.getTLogOptions().isEnabled()) {
throw new KeylessVerificationException(
"No transparency log entry found and transparency-log verification is required");
}
if (!entries.isEmpty() && !options.getTLogOptions().isEnabled()) {
throw new KeylessVerificationException(
"Bundle contains a transparency log entry but transparency-log verification is disabled");
}
Comment thread
anthonydahanne marked this conversation as resolved.
var rekorEntry = entries.isEmpty() ? null : entries.get(0);

byte[] signature;
if (bundle.getMessageSignature().isPresent()) { // hashedrekord
Expand All @@ -222,16 +234,20 @@ public void verify(byte[] artifactDigest, Bundle bundle, VerificationOptions opt
signature = dsseEnvelope.getSignature();
}

try {
rekorVerifier.verifyEntry(rekorEntry);
} catch (RekorVerificationException ex) {
throw new KeylessVerificationException("Transparency log entry could not be verified", ex);
// A transparency-log entry that is present is always verified; opting in only relaxes the
// requirement that one exists, never the verification of one that is provided.
Instant entryTime = null;
if (rekorEntry != null) {
try {
rekorVerifier.verifyEntry(rekorEntry);
} catch (RekorVerificationException ex) {
throw new KeylessVerificationException("Transparency log entry could not be verified", ex);
}
// if entry was verified and has a SET, get time from it
var set = rekorEntry.getVerification().getSignedEntryTimestamp();
entryTime = set != null ? rekorEntry.getIntegratedTimeInstant() : null;
}

// if entry was verified and has a SET, get time from it
var set = rekorEntry.getVerification().getSignedEntryTimestamp();
var entryTime = set != null ? rekorEntry.getIntegratedTimeInstant() : null;

verifyTimestamps(leafCert, bundle.getTimestamps(), entryTime, signature);
}

Expand Down Expand Up @@ -325,6 +341,12 @@ private void checkMessageSignature(
"Signature could not be processed: " + ex.getMessage(), ex);
}

// No transparency-log entry (allowed via VerificationOptions): the artifact digest and
// signature have been verified above, and there is nothing to cross-check against.
if (rekorEntry == null) {
return;
}

// recreate the log entry and check if it matches what was provided in the entry
String version;
try {
Expand Down Expand Up @@ -469,6 +491,12 @@ private void checkDsseEnvelope(
throw new KeylessVerificationException("Signature could not be processed", se);
}

// No transparency-log entry (allowed via VerificationOptions): the payload subject digest and
// DSSE signature have been verified above, and there is nothing to cross-check against.
if (rekorEntry == null) {
return;
}

RekorEntryBody entryBody;
try {
entryBody = rekorEntry.getBodyDecoded();
Expand Down
35 changes: 35 additions & 0 deletions sigstore-java/src/main/java/dev/sigstore/VerificationOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.function.Predicate;
import org.immutables.value.Value.Default;
import org.immutables.value.Value.Immutable;

@Immutable(singleton = true)
Expand All @@ -28,6 +29,40 @@ public interface VerificationOptions {
/** An allow list of certificate identities to match with. */
List<CertificateMatcher> getCertificateMatchers();

/** Transparency-log (rekor) verification policy; defaults to requiring an entry. */
@Default
default TLogOptions getTLogOptions() {
return TLogOptions.builder().isEnabled(true).build();
}

/** Certificate-transparency (SCT) verification policy; defaults to requiring an SCT. */
@Default
default CTLogOptions getCtLogOptions() {
return CTLogOptions.builder().isEnabled(true).build();
}

/** Transparency-log verification options. */
@Immutable
interface TLogOptions {
/** Whether a transparency-log entry is required and verified. */
boolean isEnabled();

static ImmutableTLogOptions.Builder builder() {
return ImmutableTLogOptions.builder();
}
}

/** Certificate-transparency verification options. */
@Immutable
interface CTLogOptions {
/** Whether a certificate-transparency log entry (SCT) is required and verified. */
boolean isEnabled();

static ImmutableCTLogOptions.Builder builder() {
return ImmutableCTLogOptions.builder();
}
}

/**
* An interface for allowing matching of certificates. Use {@link #fulcio()} to instantiate the
* default {@link FulcioCertificateMatcher} implementation. Custom implementations may throw
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ static Bundle readBundle(Reader jsonReader) throws BundleParseException {

bundleBuilder.mediaType(protoBundle.getMediaType());

if (protoBundle.getVerificationMaterial().getTlogEntriesCount() == 0) {
throw new BundleParseException("Could not find any tlog entries in bundle json");
}
for (var bundleEntry : protoBundle.getVerificationMaterial().getTlogEntriesList()) {
if (!bundleEntry.hasInclusionProof()) {
// all consumed bundles must have an inclusion proof
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package dev.sigstore.fulcio.client;

import com.google.common.annotations.VisibleForTesting;
import dev.sigstore.VerificationOptions.CTLogOptions;
import dev.sigstore.encryption.certificates.Certificates;
import dev.sigstore.encryption.certificates.transparency.CTLogInfo;
import dev.sigstore.encryption.certificates.transparency.CTVerificationResult;
Expand Down Expand Up @@ -44,7 +45,7 @@
import java.util.Map;
import java.util.stream.Collectors;

/** Verifier for fulcio generated signing cerificates */
/** Verifier for fulcio generated signing certificates */
public class FulcioVerifier {
private final List<CertificateAuthority> cas;
private final List<TransparencyLog> ctLogs;
Expand Down Expand Up @@ -94,8 +95,13 @@ private FulcioVerifier(
}

@VisibleForTesting
void verifySct(CertPath fullCertPath) throws FulcioVerificationException {
if (ctLogs.size() == 0) {
void verifySct(CertPath fullCertPath, CTLogOptions ctLogOptions)
throws FulcioVerificationException {
if (ctLogs.isEmpty()) {
// Only when there are no CT Logs (private deployment) we can skip SCT verification
if (!ctLogOptions.isEnabled()) {
return;
}
throw new FulcioVerificationException("No ct logs were provided to verifier");
}

Expand Down Expand Up @@ -150,8 +156,17 @@ private void verifyEmbeddedScts(CertPath certPath) throws FulcioVerificationExce
*/
public void verifySigningCertificate(CertPath signingCertificate)
throws FulcioVerificationException, IOException {
verifySigningCertificate(signingCertificate, CTLogOptions.builder().isEnabled(true).build());
}

/**
* Verify a signing certificate, applying the given certificate-transparency policy (see {@link
* #verifySct(CertPath, CTLogOptions)}).
*/
public void verifySigningCertificate(CertPath signingCertificate, CTLogOptions ctLogOptions)
throws FulcioVerificationException, IOException {
CertPath fullCertPath = validateCertPath(signingCertificate);
verifySct(fullCertPath);
verifySct(fullCertPath, ctLogOptions);
}

public CertPath trimTrustedParent(CertPath signingCertificate)
Expand All @@ -177,7 +192,8 @@ CertPath validateCertPath(CertPath signingCertificate) throws FulcioVerification
} 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",
"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);
}

Expand All @@ -197,7 +213,8 @@ CertPath validateCertPath(CertPath signingCertificate) throws FulcioVerification
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",
"Can't create PKIX parameters for fulcioRoot. This should have been checked when"
+ " generating a verifier instance",
e);
}
pkixParams.setRevocationEnabled(false);
Expand Down
Loading