CertificateEntry.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.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
/**
* CertificateEntry structure. This structure describes part of the data which is signed over in
* SCTs. It is not defined by the RFC6962, but it is useful to have.
*
* <p>It's definition would be : struct { LogEntryType entry_type; select(entry_type) { case
* x509_entry: ASN.1Cert; case precert_entry: PreCert; } signed_entry; } CertificateEntry;
*/
public class CertificateEntry {
public enum LogEntryType {
X509_ENTRY,
PRECERT_ENTRY
}
private final LogEntryType entryType;
// Only used when entryType is LOG_ENTRY_TYPE_PRECERT
private final byte[] issuerKeyHash;
/* If entryType == PRECERT_ENTRY, this is the encoded TBS of the precertificate.
If entryType == X509_ENTRY, this is the encoded leaf certificate. */
private final byte[] certificate;
private CertificateEntry(LogEntryType entryType, byte[] certificate, byte[] issuerKeyHash) {
if (entryType == LogEntryType.PRECERT_ENTRY && issuerKeyHash == null) {
throw new IllegalArgumentException("issuerKeyHash missing for precert entry.");
} else if (entryType == LogEntryType.X509_ENTRY && issuerKeyHash != null) {
throw new IllegalArgumentException("unexpected issuerKeyHash for X509 entry.");
}
if (issuerKeyHash != null && issuerKeyHash.length != CTConstants.ISSUER_KEY_HASH_LENGTH) {
throw new IllegalArgumentException("issuerKeyHash must be 32 bytes long");
}
this.entryType = entryType;
this.issuerKeyHash = issuerKeyHash;
this.certificate = certificate;
}
/**
* Creates a CertificateEntry with type PRECERT_ENTRY
*
* @throws IllegalArgumentException if issuerKeyHash isn't 32 bytes
*/
public static CertificateEntry createForPrecertificate(
byte[] tbsCertificate, byte[] issuerKeyHash) {
return new CertificateEntry(LogEntryType.PRECERT_ENTRY, tbsCertificate, issuerKeyHash);
}
public static CertificateEntry createForPrecertificate(
X509Certificate leaf, X509Certificate issuer) throws CertificateException {
try {
if (!leaf.getNonCriticalExtensionOIDs().contains(CTConstants.X509_SCT_LIST_OID)) {
throw new CertificateException("Certificate does not contain embedded signed timestamps");
}
var bcCert = Certificate.getInstance(leaf.getEncoded());
var extensions = bcCert.getTBSCertificate().getExtensions();
var filteredExtensionsList =
Arrays.stream(extensions.getExtensionOIDs())
.sequential()
.filter(oid -> !oid.getId().equals(CTConstants.X509_SCT_LIST_OID))
.filter(oid -> !oid.getId().equals(CTConstants.POISON_EXTENSION_OID))
.map(extensions::getExtension)
.toArray(Extension[]::new);
var filteredExtensions = new Extensions(filteredExtensionsList);
var tbs = bcCert.getTBSCertificate();
var tbsGenerator = new V3TBSCertificateGenerator();
tbsGenerator.setSerialNumber(tbs.getSerialNumber());
tbsGenerator.setSignature(tbs.getSignature());
tbsGenerator.setIssuer(tbs.getIssuer());
tbsGenerator.setStartDate(tbs.getStartDate());
tbsGenerator.setEndDate(tbs.getEndDate());
tbsGenerator.setSubject(tbs.getSubject());
tbsGenerator.setSubjectPublicKeyInfo(tbs.getSubjectPublicKeyInfo());
tbsGenerator.setIssuerUniqueID((DERBitString) tbs.getIssuerUniqueId());
tbsGenerator.setSubjectUniqueID((DERBitString) tbs.getSubjectUniqueId());
tbsGenerator.setExtensions(filteredExtensions);
var precertTbs = tbsGenerator.generateTBSCertificate();
byte[] issuerKey = issuer.getPublicKey().getEncoded();
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(issuerKey);
byte[] issuerKeyHash = md.digest();
return createForPrecertificate(precertTbs.getEncoded(), issuerKeyHash);
} catch (NoSuchAlgorithmException e) {
// SHA-256 is guaranteed to be available
throw new RuntimeException(e);
} catch (IOException ex) {
throw new CertificateException("Could not create precertificate", ex);
}
}
public static CertificateEntry createForX509Certificate(byte[] x509Certificate) {
return new CertificateEntry(LogEntryType.X509_ENTRY, x509Certificate, null);
}
public static CertificateEntry createForX509Certificate(X509Certificate cert)
throws CertificateEncodingException {
return createForX509Certificate(cert.getEncoded());
}
public LogEntryType getEntryType() {
return entryType;
}
public byte[] getCertificate() {
return certificate;
}
public byte[] getIssuerKeyHash() {
return issuerKeyHash;
}
/** TLS encode the CertificateEntry structure. */
@SuppressWarnings("EnumOrdinal")
public void encode(OutputStream output) throws SerializationException {
Serialization.writeNumber(output, entryType.ordinal(), CTConstants.LOG_ENTRY_TYPE_LENGTH);
if (entryType == LogEntryType.PRECERT_ENTRY) {
Serialization.writeFixedBytes(output, issuerKeyHash);
}
Serialization.writeVariableBytes(output, certificate, CTConstants.CERTIFICATE_LENGTH_BYTES);
}
}