RekorVerifier.java
/*
* 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.merkle.InclusionProofVerificationException;
import dev.sigstore.merkle.InclusionProofVerifier;
import dev.sigstore.rekor.client.RekorEntry.Checkpoint;
import dev.sigstore.rekor.client.RekorEntry.CheckpointSignature;
import dev.sigstore.trustroot.SigstoreTrustedRoot;
import dev.sigstore.trustroot.TransparencyLog;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import org.bouncycastle.util.encoders.Hex;
/** Verifier for rekor entries. */
public class RekorVerifier {
private final List<TransparencyLog> tlogs;
public static RekorVerifier newRekorVerifier(SigstoreTrustedRoot trustRoot) {
return newRekorVerifier(trustRoot.getTLogs());
}
public static RekorVerifier newRekorVerifier(List<TransparencyLog> tlogs) {
return new RekorVerifier(tlogs);
}
private RekorVerifier(List<TransparencyLog> tlogs) {
this.tlogs = tlogs;
}
/**
* Verifies a Rekor entry by checking the inclusion proof and checkpoint against the configured
* transparency logs. For v1 entries, it also verifies the Signed Entry Timestamp (SET).
*
* @param entry The RekorEntry to verify.
* @throws RekorVerificationException if the entry cannot be verified for any reason, such as an
* invalid proof or signature.
*/
public void verifyEntry(RekorEntry entry) throws RekorVerificationException {
if (entry.getVerification().getInclusionProof() == null) {
throw new RekorVerificationException("No inclusion proof in entry.");
}
var tlog =
TransparencyLog.find(tlogs, Hex.decode(entry.getLogID()))
.orElseThrow(
() ->
new RekorVerificationException(
"Log entry (logid) does not match any provided transparency logs."));
var set = entry.getVerification().getSignedEntryTimestamp();
if (set != null && !set.isEmpty()) {
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
| InvalidKeyException
| SignatureException
| NoSuchAlgorithmException e) {
throw new RekorVerificationException("Entry SET verification failed: " + e.getMessage(), e);
}
}
// verify inclusion proof
verifyInclusionProof(entry);
verifyCheckpoint(entry, tlog);
}
/** Verify that a Rekor Entry is in the log by checking inclusion proof. */
private void verifyInclusionProof(RekorEntry entry) throws RekorVerificationException {
var inclusionProof = entry.getVerification().getInclusionProof();
var leafHash =
Hashing.sha256()
.newHasher()
.putByte((byte) 0x00)
.putBytes(Base64.getDecoder().decode(entry.getBody()))
.hash()
.asBytes();
List<byte[]> hashes = new ArrayList<>();
for (String hash : inclusionProof.getHashes()) {
hashes.add(Hex.decode(hash));
}
byte[] expectedRootHash = Hex.decode(inclusionProof.getRootHash());
try {
InclusionProofVerifier.verify(
leafHash,
inclusionProof.getLogIndex(),
inclusionProof.getTreeSize(),
hashes,
expectedRootHash);
} catch (InclusionProofVerificationException e) {
throw new RekorVerificationException("Inclusion proof verification failed", e);
}
}
private void verifyCheckpoint(RekorEntry entry, TransparencyLog tlog)
throws RekorVerificationException {
Checkpoint parsedCheckpoint;
try {
parsedCheckpoint = entry.getVerification().getInclusionProof().parsedCheckpoint();
} catch (RekorParseException ex) {
throw new RekorVerificationException("Could not parse checkpoint from envelope", ex);
}
final int MAX_CHECKPOINT_SIGNATURES = 20;
if (parsedCheckpoint.getSignatures().size() > MAX_CHECKPOINT_SIGNATURES) {
throw new RekorVerificationException(
"Checkpoint contains an excessive number of signatures ("
+ parsedCheckpoint.getSignatures().size()
+ "), exceeding the maximum allowed of "
+ MAX_CHECKPOINT_SIGNATURES);
}
byte[] inclusionRootHash =
Hex.decode(entry.getVerification().getInclusionProof().getRootHash());
byte[] checkpointRootHash = Base64.getDecoder().decode(parsedCheckpoint.getBase64Hash());
if (!Arrays.equals(inclusionRootHash, checkpointRootHash)) {
throw new RekorVerificationException(
"Checkpoint root hash does not match root hash provided in inclusion proof");
}
Optional<CheckpointSignature> matchingSig =
parsedCheckpoint.getSignatures().stream()
.filter(sig -> sig.getIdentity().equals(tlog.getBaseUrl().getHost()))
.findFirst();
if (!matchingSig.isPresent()) {
throw new RekorVerificationException(
"No matching checkpoint signature found for transparency log: "
+ tlog.getBaseUrl().getHost());
}
var keyId = tlog.getLogId().getKeyId();
var keyHint = Arrays.copyOfRange(keyId, 0, 4);
if (!Arrays.equals(matchingSig.get().getKeyHint(), keyHint)) {
throw new RekorVerificationException(
"Checkpoint key hint did not match provided log public key");
}
var signedData = parsedCheckpoint.getSignedData();
try {
if (!Verifiers.newVerifier(tlog.getPublicKey().toJavaPublicKey())
.verify(signedData.getBytes(StandardCharsets.UTF_8), matchingSig.get().getSignature())) {
throw new RekorVerificationException("Checkpoint signature was invalid");
}
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| SignatureException
| InvalidKeyException ex) {
throw new RekorVerificationException("Could not verify checkpoint signature", ex);
}
}
}