diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/IDevIDCertificate.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/IDevIDCertificate.java index 2a30a6d4c..0d7d9c968 100644 --- a/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/IDevIDCertificate.java +++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/IDevIDCertificate.java @@ -1,12 +1,15 @@ package hirs.attestationca.persist.entity.userdefined.certificate; -import hirs.attestationca.persist.entity.userdefined.Certificate; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Transient; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.log4j.Log4j2; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import hirs.attestationca.persist.entity.userdefined.certificate.attributes.DiceCertificateInfo; +import jakarta.persistence.PostLoad; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; @@ -18,13 +21,14 @@ import org.bouncycastle.asn1.x509.PolicyInformation; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import hirs.attestationca.persist.entity.userdefined.Certificate; +import hirs.attestationca.persist.entity.userdefined.certificate.attributes.DiceCertificateParser; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Transient; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; @Entity @Getter @@ -46,9 +50,19 @@ 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). */ @@ -67,6 +81,11 @@ public class IDevIDCertificate extends Certificate { @Column private String tpmPolicies; + /** + * Serial version UID for serialization. + */ + private static final long serialVersionUID = 9223372036854775807L; + /** * Construct a new IDevIDCertificate given its binary contents. The given * certificate should represent a valid X.509 certificate. @@ -123,36 +142,40 @@ public Map getTPMPolicyQualifiers(final byte[] policyBytes) thr for (PolicyInformation policy : certPolicies.getPolicyInformation()) { // Add the data based on the OIDs switch (policy.getPolicyIdentifier().toString()) { - case POLICY_QUALIFIER_VERIFIED_TPM_RESIDENCY: - verifiedTPMResidency = true; - break; - case POLICY_QUALIFIER_VERIFIED_TPM_FIXED: - verifiedTPMFixed = true; - break; - case POLICY_QUALIFIER_VERIFIED_TPM_RESTRICTED: - verifiedTPMRestricted = true; - break; - default: - break; + case POLICY_QUALIFIER_VERIFIED_TPM_RESIDENCY -> verifiedTPMResidency = true; + case POLICY_QUALIFIER_VERIFIED_TPM_FIXED -> verifiedTPMFixed = true; + case POLICY_QUALIFIER_VERIFIED_TPM_RESTRICTED -> verifiedTPMRestricted = true; + default -> { /* No action needed for unknown policies */ } } } } // 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. * * @throws IOException if a problem is encountered during parsing */ private void parseIDevIDCertificate() throws IOException { - this.subjectAltName = getX509Certificate().getExtensionValue(SUBJECT_ALTERNATIVE_NAME_EXTENSION); diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceCertificateInfo.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceCertificateInfo.java new file mode 100644 index 000000000..ec732a772 --- /dev/null +++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceCertificateInfo.java @@ -0,0 +1,54 @@ +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 TCG DICE Certificate + * Profiles specification + */ +@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 diceKeyPurposes; + /** + * The CA flag of the certificate. + */ + private boolean isCa; + /** + * The keyCertSign flag of the certificate. + */ + @Getter(AccessLevel.NONE) + private boolean hasKeyCertSign; + /** + * The CRL Sign 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 keyCertSign flag of this certificate. + * @return the keyCertSign boolean value + */ + public boolean hasCrlSign() { + return hasCrlSign; + } +} diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceCertificateParser.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceCertificateParser.java new file mode 100644 index 000000000..46daa9f04 --- /dev/null +++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceCertificateParser.java @@ -0,0 +1,146 @@ +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 TCG DICE Certificate + * Profiles specification + */ +@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 cer\t 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 diceKeyPurposes; + boolean isCa; + boolean hasKeyCertSign; + boolean hasCrlSign; + + // Extended Key Usage: map DICE OIDs. + List 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 input CA certificate field + * @param hasKeyCertSign input keyCertSign field + * @return an output DICE profile type + */ + private static DiceProfileType classifyProfile(final Set 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 extractKeyPurposes(final List ekuOids) { + Set diceKeyPurposes = EnumSet.noneOf(DiceKeyPurpose.class); + + for (String oid : ekuOids) { + DiceKeyPurpose kp = DiceKeyPurpose.fromOid(oid); + + if (kp != DiceKeyPurpose.OTHER) { + diceKeyPurposes.add(kp); + } + } + + return diceKeyPurposes; + } +} diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceKeyPurpose.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceKeyPurpose.java new file mode 100644 index 000000000..2121e96f0 --- /dev/null +++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceKeyPurpose.java @@ -0,0 +1,80 @@ +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 BY_OID; + + static { + Map 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 thjs 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 getExtendedKeyUsageMap() { + Map ekuMap = new HashMap<>(); + for (DiceKeyPurpose value : values()) { + ekuMap.put(value.oid, value.displayName); + } + return Collections.unmodifiableMap(ekuMap); + } +} diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceProfileType.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceProfileType.java new file mode 100644 index 000000000..2fcaf7b4b --- /dev/null +++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/persist/entity/userdefined/certificate/attributes/DiceProfileType.java @@ -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 +} diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/utils/CertificateStringMapBuilder.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/utils/CertificateStringMapBuilder.java index 34c83dea4..7edb15fd8 100644 --- a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/utils/CertificateStringMapBuilder.java +++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/utils/CertificateStringMapBuilder.java @@ -11,6 +11,7 @@ import hirs.attestationca.persist.entity.userdefined.certificate.IssuedAttestationCertificate; import hirs.attestationca.persist.entity.userdefined.certificate.PlatformCredential; import hirs.attestationca.persist.entity.userdefined.certificate.attributes.ComponentIdentifier; +import hirs.attestationca.persist.entity.userdefined.certificate.attributes.DiceKeyPurpose; import hirs.attestationca.persist.entity.userdefined.certificate.attributes.PlatformConfigurationV1; import hirs.attestationca.persist.entity.userdefined.certificate.attributes.V2.ComponentIdentifierV2; import hirs.attestationca.persist.entity.userdefined.certificate.attributes.V2.PlatformConfigurationV2; @@ -39,6 +40,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import static hirs.utils.specificationLookups.PlatformClass.getPlatClassFromId; @@ -69,6 +71,7 @@ private static Map getExtendedKeyUsageMap() { ekuMap.put(TCG_KP_AIK_CERTIFICATE, "tcg-kp-AIKCertificate"); ekuMap.put(TCG_KP_PLATFORM_KEY_CERTIFICATE, "tcg-kp-PlatformKeyCertificate"); ekuMap.put(TCG_KP_DELTA_PLATFORM_ATTRIBUTE_CERTIFICATE, "tcg-kp-DeltaPlatformAttributeCertificate"); + ekuMap.putAll(DiceKeyPurpose.getExtendedKeyUsageMap()); return ekuMap; } @@ -159,14 +162,15 @@ public static HashMap getGeneralCertificateInfo( data.put("keyUsage", certificate.getKeyUsage()); } - if (certificate.getExtendedKeyUsage() != null - && !certificate.getExtendedKeyUsage().isEmpty()) { - String eku = certificate.getExtendedKeyUsage().replaceAll("\\n$", ""); - if (ekuMap.containsKey(eku)) { - data.put("extendedKeyUsage", eku + " (" + ekuMap.get(eku) + ")"); - } else { - data.put("extendedKeyUsage", eku + " (Warning: Unexpected OID)"); - } + // Parse EKU OIDs into a comma-delimited list + if (certificate.getExtendedKeyUsage() != null && !certificate.getExtendedKeyUsage().isEmpty()) { + String[] oids = certificate.getExtendedKeyUsage().split("\\n"); + String displayNames = Arrays.stream(oids) + .filter(oid -> !oid.isBlank()) + .map(oid -> ekuMap.containsKey(oid) ? oid + " (" + ekuMap.get(oid) + ")" + : oid + " (Warning: Unexpected OID)") + .collect(Collectors.joining(", ")); + data.put("extendedKeyUsage", displayNames); } //Get issuer ID if not self signed @@ -819,18 +823,11 @@ public static HashMap getIdevidInformation(final UUID uuid, } } - if (certificate.getKeyUsage() != null) { - data.put("keyUsage", certificate.getKeyUsage()); - } - - if (certificate.getExtendedKeyUsage() != null - && !certificate.getExtendedKeyUsage().isEmpty()) { - String eku = certificate.getExtendedKeyUsage().replaceAll("\\n$", ""); - if (ekuMap.containsKey(eku)) { - data.put("extendedKeyUsage", eku + " (" + ekuMap.get(eku) + ")"); - } else { - data.put("extendedKeyUsage", eku + " (Warning: Unexpected OID)"); - } + if (certificate.getDiceCertificateInfo() != null) { + data.put("diceProfileType", certificate.getDiceCertificateInfo().getProfileType().toString()); + data.put("diceHasKeyCertSign", certificate.getDiceCertificateInfo().hasKeyCertSign() ? "Yes" : "No"); + data.put("diceIsCa", certificate.getDiceCertificateInfo().isCa() ? "Yes" : "No"); + data.put("diceHasCrlSign", certificate.getDiceCertificateInfo().hasCrlSign() ? "Yes" : "No"); } if (certificate.getTpmPolicies() != null) { diff --git a/HIRS_AttestationCAPortal/src/main/resources/templates/certificate-details.html b/HIRS_AttestationCAPortal/src/main/resources/templates/certificate-details.html index bdc8a5b48..7efe42f35 100644 --- a/HIRS_AttestationCAPortal/src/main/resources/templates/certificate-details.html +++ b/HIRS_AttestationCAPortal/src/main/resources/templates/certificate-details.html @@ -1836,6 +1836,53 @@

[[${initialData.get('extendedKeyUsage')}]] + + +
+
+ + DICE Certificate Info + +
+
+ + + + + + + + + + + + + + + + + +
Profile Type + +
Has Key Cert Sign + +
Has CRL Sign + +
Is CA + +
+
+
+