Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package hirs.attestationca.persist.entity.userdefined.certificate;

import hirs.attestationca.persist.entity.userdefined.Certificate;
import hirs.attestationca.persist.entity.userdefined.certificate.attributes.DiceCertificateInfo;
import hirs.attestationca.persist.entity.userdefined.certificate.attributes.DiceCertificateParser;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Transient;
import jakarta.persistence.PostLoad;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
Expand All @@ -20,6 +23,7 @@

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serial;
import java.nio.file.Path;
import java.time.Instant;
import java.util.HashMap;
Expand All @@ -46,9 +50,17 @@ public class IDevIDCertificate extends Certificate {
private static final String POLICY_QUALIFIER_VERIFIED_TPM_FIXED = "2.23.133.11.1.2";
private static final String POLICY_QUALIFIER_VERIFIED_TPM_RESTRICTED = "2.23.133.11.1.3";

/**
* The raw byte array of the subject alternative name extension, if present.
* This will be null if the certificate does not contain a subject alternative name extension.
*/
@Transient
private byte[] subjectAltName;

/** Parsed DICE attributes from the certificate, if present. */
@Transient
private transient DiceCertificateInfo diceCertificateInfo;

/**
* Corresponds to the hwType field found in a Hardware Module Name (if present).
*/
Expand All @@ -67,6 +79,10 @@ public class IDevIDCertificate extends Certificate {
@Column
private String tpmPolicies;

/** Serial version UID for serialization. */
@Serial
private static final long serialVersionUID = 9223372036854775807L;

/**
* Construct a new IDevIDCertificate given its binary contents. The given
* certificate should represent a valid X.509 certificate.
Expand Down Expand Up @@ -139,13 +155,23 @@ public Map<String, Boolean> getTPMPolicyQualifiers(final byte[] policyBytes) thr
}

// Add to map
policyQualifiers.put("verifiedTPMResidency", Boolean.valueOf(verifiedTPMResidency));
policyQualifiers.put("verifiedTPMFixed", Boolean.valueOf(verifiedTPMFixed));
policyQualifiers.put("verifiedTPMRestricted", Boolean.valueOf(verifiedTPMRestricted));
policyQualifiers.put("verifiedTPMResidency", verifiedTPMResidency);
policyQualifiers.put("verifiedTPMFixed", verifiedTPMFixed);
policyQualifiers.put("verifiedTPMRestricted", verifiedTPMRestricted);

return policyQualifiers;
}

/**
* Helper function to parse transient fields after load.
* @throws IOException if there is an exception during parsing.
*/
@PostLoad
private void parseTransientFields() throws IOException {
this.diceCertificateInfo = DiceCertificateParser.parse(this.getX509Certificate());
this.subjectAltName = getX509Certificate().getExtensionValue(SUBJECT_ALTERNATIVE_NAME_EXTENSION);
}

/**
* Parses fields related to IDevID certificates.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package hirs.attestationca.persist.entity.userdefined.certificate.attributes;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Set;

/**
* Contains information about a DICE certificate.
* @see <a href="https://trustedcomputinggroup.org/resource/dice-certificate-profiles/">TCG DICE Certificate
* Profiles specification</a>
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public final class DiceCertificateInfo {
/** The DICE profile type of the certificate. */
private DiceProfileType profileType;
/** The DICE key purposes of the certificate. */
private Set<DiceKeyPurpose> diceKeyPurposes;
/** The CA flag of the certificate. */
private boolean isCa;
/** The keyCertSign flag of the certificate. */
@Getter(AccessLevel.NONE)
private boolean hasKeyCertSign;
/** The cRLSign flag of the certificate. */
@Getter(AccessLevel.NONE)
private boolean hasCrlSign;

/**
* Returns the keyCertSign flag of this certificate.
* @return the keyCertSign boolean value
*/
public boolean hasKeyCertSign() {
return hasKeyCertSign;
}
/**
* Returns the cRLSign flag of this certificate.
* @return the cRLSign boolean value
*/
public boolean hasCrlSign() {
return hasCrlSign;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package hirs.attestationca.persist.entity.userdefined.certificate.attributes;

import lombok.extern.log4j.Log4j2;

import java.io.IOException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

/**
* Utility class for parsing and analyzing DICE (Device Identifier Composition Engine) certificate attributes.
* Provides methods to extract DICE-specific information from X.509 certificates and classify them according
* to TCG DICE certificate profiles.
* @see <a href="https://trustedcomputinggroup.org/resource/dice-certificate-profiles/">TCG DICE Certificate
* Profiles specification</a>
*/
@Log4j2
public final class DiceCertificateParser {
/** Private constructor to prevent instantiation of utility class. */
private DiceCertificateParser() {
throw new UnsupportedOperationException("Utility class should not be instantiated");
}

/** Key usage bit position for keyCertSign (bit 5). */
private static final int KEY_CERT_SIGN_BIT = 5;
/** Key usage bit position for cRLSign (bit 6). */
private static final int CRL_SIGN_BIT = 6;

/**
* Parses a DICE certificate and extracts relevant attributes.
* @param cert the X.509 certificate to parse
* @return a {@link DiceCertificateInfo} object containing the extracted attributes, or null if invalid
* @throws IOException if certificate parsing fails
*/
public static DiceCertificateInfo parse(final X509Certificate cert) throws IOException {
if (cert == null) {
throw new IOException("Certificate must be an X.509 certificate");
}

DiceProfileType profileType;
Set<DiceKeyPurpose> diceKeyPurposes;
boolean isCa;
boolean hasKeyCertSign;
boolean hasCrlSign;

// Extended Key Usage: map DICE OIDs.
List<String> ekuOids;

try {
ekuOids = cert.getExtendedKeyUsage();
} catch (CertificateParsingException e) {
log.warn("DICE certificate contains invalid OIDs");
return null;
}

if (ekuOids != null) {
diceKeyPurposes = extractKeyPurposes(ekuOids);
} else {
return null; // Not a DICE certificate
}

// Basic constraints and key usage.
int bc = cert.getBasicConstraints();
isCa = (bc >= 0);

boolean[] ku = cert.getKeyUsage();

if (ku != null && ku.length > 0) {
// keyCertSign is bit 5, cRLSign is bit 6 (0‑based index).
hasKeyCertSign = ku.length > KEY_CERT_SIGN_BIT && ku[KEY_CERT_SIGN_BIT];
hasCrlSign = ku.length > CRL_SIGN_BIT && ku[CRL_SIGN_BIT];
} else {
hasKeyCertSign = false;
hasCrlSign = false;
}

// Rough classification based on key purposes (tables 1–4).
profileType = classifyProfile(diceKeyPurposes, isCa, hasKeyCertSign);

return new DiceCertificateInfo(profileType, diceKeyPurposes, isCa, hasKeyCertSign, hasCrlSign);
}

/**
* Classifies a DICE certificate profile based on key purposes and related attributes.
* @param keyPurposes the key purposes to classify
* @param isCa true if a CA certificate
* @param hasKeyCertSign true if cert contains keyCertSign
* @return an output DICE profile type
*/
private static DiceProfileType classifyProfile(final Set<DiceKeyPurpose> keyPurposes,
final boolean isCa, final boolean hasKeyCertSign) {
boolean hasIdentityInit = keyPurposes.contains(DiceKeyPurpose.IDENTITY_INIT);
boolean hasIdentityLoc = keyPurposes.contains(DiceKeyPurpose.IDENTITY_LOC);
boolean hasAttestInit = keyPurposes.contains(DiceKeyPurpose.ATTEST_INIT);
boolean hasAttestLoc = keyPurposes.contains(DiceKeyPurpose.ATTEST_LOC);
boolean hasEca = keyPurposes.contains(DiceKeyPurpose.ECA);

DiceProfileType profileType;

// ECA certificate profile.
if (hasEca && isCa && hasKeyCertSign) {
profileType = DiceProfileType.ECA;
// Attestation certificate profile (5.1.6.4).
} else if (hasAttestInit || hasAttestLoc) {
profileType = DiceProfileType.ATTESTATION;
// Profiles per 5.1.6.
} else if (hasIdentityInit) {
profileType = DiceProfileType.IDevID;
} else if (hasIdentityLoc) {
profileType = DiceProfileType.LDevID;
} else {
profileType = DiceProfileType.UNKNOWN;
}

return profileType;
}

/**
* Helper method to extract DICE key purposes from a given list of OIDs.
* @param ekuOids the input list of OIDs
* @return a {@link Set} containing the corresponding key purposes
*/
private static Set<DiceKeyPurpose> extractKeyPurposes(final List<String> ekuOids) {
Set<DiceKeyPurpose> diceKeyPurposes = EnumSet.noneOf(DiceKeyPurpose.class);

for (String oid : ekuOids) {
DiceKeyPurpose kp = DiceKeyPurpose.fromOid(oid);

if (kp != DiceKeyPurpose.OTHER) {
diceKeyPurposes.add(kp);
}
}

return diceKeyPurposes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package hirs.attestationca.persist.entity.userdefined.certificate.attributes;

import lombok.Getter;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Enumeration of DICE key purposes as defined in the TCG specification "DICE Certificate Profiles".
* Contains DICE EKU OID mappings for each purpose.
*/
public enum DiceKeyPurpose {
/** Initial Identity key purpose. */
IDENTITY_INIT("2.23.133.5.4.100.6", "DICE Initial Identity"),
/** Local Identity key purpose. */
IDENTITY_LOC("2.23.133.5.4.100.7", "DICE Local Identity"),
/** Initial Attestation key purpose. */
ATTEST_INIT("2.23.133.5.4.100.8", "DICE Initial Attestation"),
/** Local Attestation key purpose. */
ATTEST_LOC("2.23.133.5.4.100.9", "DICE Local Attestation"),
/** Initial Assertion key purpose. */
ASSERT_INIT("2.23.133.5.4.100.10", "DICE Initial Assertion"),
/** Local Assertion key purpose. */
ASSERT_LOC("2.23.133.5.4.100.11", "DICE Local Assertion"),
/** ECA (Embedded Certificate Authority) key purpose. */
ECA("2.23.133.5.4.100.12", "DICE Embedded Certificate Authority"),
/** Other key purposes not specifically defined. */
OTHER(null, "Other");

private static final Map<String, DiceKeyPurpose> BY_OID;

static {
Map<String, DiceKeyPurpose> byOid = new HashMap<>();
for (DiceKeyPurpose value : values()) {
if (value.oid != null) {
byOid.put(value.oid, value);
}
}
BY_OID = Collections.unmodifiableMap(byOid);
}

/** Contains the TCG DICE OID for this key purpose. */
@Getter
private final String oid;
/** Contains the display name for this key purpose. */
@Getter
private final String displayName;

DiceKeyPurpose(final String oid, final String displayName) {
this.oid = oid;
this.displayName = displayName;
}

/**
* Helper method to return a DICE key purpose from a given OID.
* @param oid the input OID
* @return An enum value corresponding to the key purpose.
*/
public static DiceKeyPurpose fromOid(final String oid) {
return BY_OID.getOrDefault(oid, OTHER);
}

/**
* Create a mapping of DICE EKU OIDs to their corresponding key purposes.
* @return An unmodifiable {@link Map} of DICE EKU OIDs to human-readable key purpose descriptions.
*/
public static Map<String, String> getExtendedKeyUsageMap() {
Map<String, String> ekuMap = new HashMap<>();
for (DiceKeyPurpose value : values()) {
ekuMap.put(value.oid, value.displayName);
}
return Collections.unmodifiableMap(ekuMap);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hirs.attestationca.persist.entity.userdefined.certificate.attributes;

/**
* Enumeration of DICE certificate profiles as defined in the TCG specification "DICE Certificate Profiles".
*/
public enum DiceProfileType {
/** IDevID (Initial Device Identifier) profile. */
IDevID,
/** LDevID (Locally Significant Device Identifier) profile. */
LDevID,
/** ECA (Embedded Certificate Authority) profile. */
ECA,
/** Attestation certificate profile. */
ATTESTATION,
/** Unknown or unclassified profile. */
UNKNOWN
}
Loading
Loading