X509ExtensionInspector.java

package io.mersel.dss.signer.api.services;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.x509.CertificatePolicies;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.PolicyInformation;
import org.bouncycastle.asn1.x509.PolicyQualifierInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

/**
 * X.509 sertifika uzantılarından (Key Usage, Extended Key Usage,
 * Certificate Policies) insan okunur özet bilgi üretir.
 *
 * Servis-agnostik: hem JCA {@link java.security.KeyStore} yolu (
 * {@link CertificateInfoService}) hem de raw PKCS#11 enumeration
 * ({@code Pkcs11TokenInspector}) bu helper'ı kullanır; bilgi formatı tek
 * yerde tanımlı olur ve iki kod yolu arasında tutarsızlık olmaz.
 */
public final class X509ExtensionInspector {

    private static final Logger LOGGER = LoggerFactory.getLogger(X509ExtensionInspector.class);

    // Policy Qualifier OID'leri (RFC 3280 §4.2.1.4)
    private static final String ID_QT_CPS = "1.3.6.1.5.5.7.2.1";
    private static final String ID_QT_UNOTICE = "1.3.6.1.5.5.7.2.2";

    private static final String[] KEY_USAGE_NAMES = {
        "Digital Signature",      // 0
        "Non Repudiation",        // 1
        "Key Encipherment",       // 2
        "Data Encipherment",      // 3
        "Key Agreement",          // 4
        "Key Cert Sign",          // 5
        "CRL Sign",               // 6
        "Encipher Only",          // 7
        "Decipher Only"           // 8
    };

    private X509ExtensionInspector() {
        // static-only
    }

    /**
     * X.509 KeyUsage uzantısını okunabilir, virgülle ayrılmış stringe çevirir.
     * @return Set edilmiş bit isimleri ("Digital Signature, Non Repudiation") veya
     *         uzantı yoksa / hata olursa {@code null}.
     */
    public static String extractKeyUsage(X509Certificate cert) {
        try {
            boolean[] keyUsage = cert.getKeyUsage();
            if (keyUsage == null) {
                return null;
            }

            List<String> usages = new ArrayList<String>();
            for (int i = 0; i < keyUsage.length && i < KEY_USAGE_NAMES.length; i++) {
                if (keyUsage[i]) {
                    usages.add(KEY_USAGE_NAMES[i]);
                }
            }
            return usages.isEmpty() ? null : String.join(", ", usages);
        } catch (Exception e) {
            LOGGER.debug("Key Usage bilgisi alınamadı: {}", e.getMessage());
            return null;
        }
    }

    /**
     * Extended Key Usage uzantısındaki OID'leri olduğu gibi döndürür.
     * @return Virgülle ayrılmış OID listesi veya {@code null}.
     */
    public static String extractExtendedKeyUsage(X509Certificate cert) {
        try {
            List<String> extKeyUsage = cert.getExtendedKeyUsage();
            if (extKeyUsage == null || extKeyUsage.isEmpty()) {
                return null;
            }
            return String.join(", ", extKeyUsage);
        } catch (Exception e) {
            LOGGER.debug("Extended Key Usage bilgisi alınamadı: {}", e.getMessage());
            return null;
        }
    }

    /**
     * Certificate Policies uzantısını parse eder; her policy için OID + opsiyonel
     * qualifier (CPS URI ya da User Notice) bilgisini birleştirir.
     *
     * @return "OID1 (qualifier1), OID2 (qualifier2)" formatında string veya {@code null}.
     */
    public static String extractCertificatePolicies(X509Certificate cert) {
        try {
            byte[] extValue = cert.getExtensionValue(Extension.certificatePolicies.getId());
            if (extValue == null) {
                return null;
            }

            ASN1Sequence sequence;
            try (ASN1InputStream outerStream = new ASN1InputStream(extValue)) {
                ASN1OctetString octets = (ASN1OctetString) outerStream.readObject();
                try (ASN1InputStream innerStream = new ASN1InputStream(octets.getOctets())) {
                    sequence = (ASN1Sequence) innerStream.readObject();
                }
            }

            CertificatePolicies policies = CertificatePolicies.getInstance(sequence);
            PolicyInformation[] policyInfos = policies.getPolicyInformation();
            if (policyInfos == null || policyInfos.length == 0) {
                return null;
            }

            List<String> policyDescriptions = new ArrayList<String>();
            for (PolicyInformation policyInfo : policyInfos) {
                ASN1ObjectIdentifier oid = policyInfo.getPolicyIdentifier();
                StringBuilder policyDesc = new StringBuilder(oid.getId());

                ASN1Sequence qualifiers = policyInfo.getPolicyQualifiers();
                if (qualifiers != null && qualifiers.size() > 0) {
                    List<String> qualifierTexts = collectPolicyQualifiers(qualifiers);
                    if (!qualifierTexts.isEmpty()) {
                        policyDesc.append(" (").append(String.join(", ", qualifierTexts)).append(")");
                    }
                }
                policyDescriptions.add(policyDesc.toString());
            }
            return policyDescriptions.isEmpty() ? null : String.join(", ", policyDescriptions);
        } catch (Exception e) {
            LOGGER.debug("Certificate Policies bilgisi alınamadı: {}", e.getMessage());
            return null;
        }
    }

    private static List<String> collectPolicyQualifiers(ASN1Sequence qualifiers) {
        List<String> qualifierTexts = new ArrayList<String>();
        for (int i = 0; i < qualifiers.size(); i++) {
            try {
                PolicyQualifierInfo qualifierInfo =
                    PolicyQualifierInfo.getInstance(qualifiers.getObjectAt(i));
                String qualifierIdStr = qualifierInfo.getPolicyQualifierId().getId();

                if (ID_QT_CPS.equals(qualifierIdStr)) {
                    ASN1String cpsUri = (ASN1String) qualifierInfo.getQualifier();
                    if (cpsUri != null) {
                        qualifierTexts.add(cpsUri.getString());
                    }
                } else if (ID_QT_UNOTICE.equals(qualifierIdStr)) {
                    ASN1Sequence userNoticeSeq = ASN1Sequence.getInstance(qualifierInfo.getQualifier());
                    if (userNoticeSeq != null && userNoticeSeq.size() > 0) {
                        for (int j = 0; j < userNoticeSeq.size(); j++) {
                            try {
                                ASN1String noticeText = (ASN1String) userNoticeSeq.getObjectAt(j);
                                if (noticeText != null) {
                                    qualifierTexts.add(noticeText.getString());
                                    break;
                                }
                            } catch (Exception ignored) {
                                // notice text tipi ASN1String değilse atla
                            }
                        }
                    }
                }
            } catch (Exception e) {
                LOGGER.debug("Policy qualifier parse edilemedi: {}", e.getMessage());
            }
        }
        return qualifierTexts;
    }
}