CTVerifier.java

/*
 * Copyright 2022 The Sigstore Authors.
 * Copyright 2015 The Android Open Source Project.
 *
 * 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.encryption.certificates.transparency;

import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CTVerifier {
  private final CTLogStore store;

  public CTVerifier(CTLogStore store) {
    this.store = store;
  }

  /**
   * Verify a certificate chain for transparency. Signed timestamps are extracted from the leaf
   * certificate and verified against the list of known logs.
   *
   * @throws IllegalArgumentException if the chain is empty
   */
  public CTVerificationResult verifySignedCertificateTimestamps(
      List<X509Certificate> chain, byte[] tlsData, byte[] ocspData)
      throws CertificateEncodingException {
    if (chain.size() == 0) {
      throw new IllegalArgumentException("Chain of certificates mustn't be empty.");
    }

    X509Certificate leaf = chain.get(0);

    CTVerificationResult result = new CTVerificationResult();
    List<SignedCertificateTimestamp> embeddedScts = getSCTsFromX509Extension(leaf);
    verifyEmbeddedSCTs(embeddedScts, chain, result);
    return result;
  }

  /**
   * Verify a list of SCTs which were embedded from an X509 certificate. The result of the
   * verification for each sct is added to {@code result}.
   */
  private void verifyEmbeddedSCTs(
      List<SignedCertificateTimestamp> scts,
      List<X509Certificate> chain,
      CTVerificationResult result) {
    // Avoid creating the cert entry if we don't need it
    if (scts.isEmpty()) {
      return;
    }

    CertificateEntry precertEntry = null;
    if (chain.size() >= 2) {
      X509Certificate leaf = chain.get(0);
      X509Certificate issuer = chain.get(1);

      try {
        precertEntry = CertificateEntry.createForPrecertificate(leaf, issuer);
      } catch (CertificateException e) {
        // Leave precertEntry as null, we handle it just below
      }
    }

    if (precertEntry == null) {
      markSCTsAsInvalid(scts, result);
      return;
    }

    for (SignedCertificateTimestamp sct : scts) {
      VerifiedSCT.Status status = verifySingleSCT(sct, precertEntry);
      result.add(new VerifiedSCT(sct, status));
    }
  }

  /** Verify a single SCT for the given Certificate Entry */
  public VerifiedSCT.Status verifySingleSCT(
      SignedCertificateTimestamp sct, CertificateEntry certEntry) {
    CTLogInfo log = store.getKnownLog(sct.getLogID());
    if (log == null) {
      return VerifiedSCT.Status.UNKNOWN_LOG;
    }

    return log.verifySingleSCT(sct, certEntry);
  }

  /** Add every SCT in {@code scts} to {@code result} with INVALID_SCT as status */
  private void markSCTsAsInvalid(
      List<SignedCertificateTimestamp> scts, CTVerificationResult result) {
    for (SignedCertificateTimestamp sct : scts) {
      result.add(new VerifiedSCT(sct, VerifiedSCT.Status.INVALID_SCT));
    }
  }

  /**
   * Parse an encoded SignedCertificateTimestampList into a list of SignedCertificateTimestamp
   * instances, as described by RFC6962. Individual SCTs which fail to be parsed are skipped. If the
   * data is null, or the encompassing list fails to be parsed, an empty list is returned.
   *
   * @param origin used to create the SignedCertificateTimestamp instances.
   */
  @SuppressWarnings("MixedMutabilityReturnType")
  private static List<SignedCertificateTimestamp> getSCTsFromSCTList(
      byte[] data, SignedCertificateTimestamp.Origin origin) {
    if (data == null) {
      return Collections.emptyList();
    }

    byte[][] sctList;
    try {
      sctList =
          Serialization.readList(
              data, CTConstants.SCT_LIST_LENGTH_BYTES, CTConstants.SERIALIZED_SCT_LENGTH_BYTES);
    } catch (SerializationException e) {
      return Collections.emptyList();
    }

    List<SignedCertificateTimestamp> scts = new ArrayList<>();
    for (byte[] encodedSCT : sctList) {
      try {
        SignedCertificateTimestamp sct = SignedCertificateTimestamp.decode(encodedSCT, origin);
        scts.add(sct);
      } catch (SerializationException e) {
        // Ignore errors
      }
    }

    return scts;
  }

  /**
   * Extract a list of SignedCertificateTimestamp embedded in an X509 certificate.
   *
   * <p>If the certificate does not contain any SCT extension, or the encompassing encoded list
   * fails to be parsed, an empty list is returned. Individual SCTs which fail to be parsed are
   * ignored.
   */
  private List<SignedCertificateTimestamp> getSCTsFromX509Extension(X509Certificate leaf) {
    byte[] extData = leaf.getExtensionValue(CTConstants.X509_SCT_LIST_OID);
    if (extData == null) {
      return Collections.emptyList();
    }

    try {
      return getSCTsFromSCTList(
          Serialization.readDEROctetString(Serialization.readDEROctetString(extData)),
          SignedCertificateTimestamp.Origin.EMBEDDED);
    } catch (SerializationException e) {
      return Collections.emptyList();
    }
  }
}