CertificateInfoService.java

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

import io.mersel.dss.signer.api.dtos.CertificateInfoDto;
import io.mersel.dss.signer.api.exceptions.KeyStoreException;
import io.mersel.dss.signer.api.services.keystore.KeyStoreProvider;
import io.mersel.dss.signer.api.services.keystore.PKCS11KeyStoreProvider;
import io.mersel.dss.signer.api.services.keystore.iaik.IaikPkcs11Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

/**
 * Keystore içerisindeki sertifika bilgilerini listeleme servisi.
 *
 * <p>Üç katmanlı kaynak sırası:</p>
 * <ol>
 *   <li><b>IAIK PKCS#11</b> (container'da {@link IaikPkcs11Module} bean'i
 *       varsa) — primary kaynak. SunPKCS11'in P11KeyStore alias map'inden
 *       bağımsızdır.</li>
 *   <li><b>JCA {@link KeyStore#aliases()}</b> — PFX gibi non-HSM
 *       sağlayıcılar için.</li>
 * </ol>
 */
@Service
public class CertificateInfoService {

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

    /** Spring inject eder; PFX yapılandırmasında {@code null}. CLI yolunda manuel set. */
    private final IaikPkcs11Module iaikModule;

    @Autowired
    public CertificateInfoService(@Autowired(required = false) IaikPkcs11Module iaikModule) {
        this.iaikModule = iaikModule;
    }

    /**
     * CLI / non-Spring kullanımı için no-arg constructor. PKCS#11 listeleme
     * istiyorsa {@link #CertificateInfoService(IaikPkcs11Module)} kullan.
     */
    public CertificateInfoService() {
        this((IaikPkcs11Module) null);
    }

    /**
     * Verilen keystore provider'dan tüm sertifikaları listeler.
     *
     * <p>Container'da {@link IaikPkcs11Module} bean'i (HSM yapılandırması)
     * mevcutsa <b>onun</b> {@code listCertificates()}'i çağrılır — token
     * üzerinde doğrudan {@code C_FindObjects}. Aksi halde JCA
     * {@code KeyStore.aliases()} fallback'i kullanılır.</p>
     */
    public List<CertificateInfoDto> listCertificates(KeyStoreProvider provider, char[] pin) {
        LOGGER.info("Keystore'dan sertifikalar listeleniyor: {}", provider.getType());

        if (iaikModule != null) {
            try {
                List<CertificateInfoDto> certs = iaikModule.listCertificates();
                LOGGER.info("IAIK üzerinden {} entry listelendi.", certs.size());
                return certs;
            } catch (Exception e) {
                // PKCS#11 yapılandırmasında PFX fallback YOL DEĞİL —
                // PKCS11KeyStoreProvider.loadKeyStore() artık her zaman
                // UnsupportedOperationException atıyor. IAIK hatasını
                // sarmalayarak yukarıya bildiriyoruz; orijinal hata mesajı
                // (HSM device error, session closed, vs.) korunsun.
                if (provider instanceof PKCS11KeyStoreProvider) {
                    throw new KeyStoreException(
                        "HSM (PKCS#11) sertifika listelemesi başarısız: " + e.getMessage()
                        + ". PKCS#11 yapılandırmasında PFX/JCA fallback yoktur — "
                        + "token bağlantısını ve PIN'i kontrol edin.", e);
                }
                LOGGER.warn("IAIK listing başarısız; JCA KeyStore.aliases() fallback'ine düşülüyor: {}",
                    e.getMessage(), e);
            }
        }

        return listViaKeyStoreAliases(provider, pin);
    }

    /**
     * Geriye dönük uyumluluk için orijinal {@link KeyStore#aliases()} tabanlı
     * enumeration. PFX/PKCS12 keystore'lar için yeterlidir; HSM'lerde alias
     * mapping kuralları nedeniyle eksik sonuç verebilir.
     */
    private List<CertificateInfoDto> listViaKeyStoreAliases(KeyStoreProvider provider, char[] pin) {
        List<CertificateInfoDto> certificates = new ArrayList<>();

        try {
            KeyStore keyStore = provider.loadKeyStore(pin);
            Enumeration<String> aliases = keyStore.aliases();
            
            while (aliases.hasMoreElements()) {
                String alias = aliases.nextElement();
                
                try {
                    // Sertifika var mı kontrol et
                    if (keyStore.isCertificateEntry(alias) || keyStore.isKeyEntry(alias)) {
                        X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);
                        
                        if (cert != null) {
                            CertificateInfoDto dto = new CertificateInfoDto();
                            dto.setAlias(alias);
                            dto.setSerialNumberHex(cert.getSerialNumber().toString(16).toUpperCase());
                            dto.setSerialNumberDec(cert.getSerialNumber().toString());
                            dto.setSubject(cert.getSubjectX500Principal().toString());
                            dto.setIssuer(cert.getIssuerX500Principal().toString());
                            dto.setValidFrom(cert.getNotBefore());
                            dto.setValidTo(cert.getNotAfter());
                            dto.setHasPrivateKey(keyStore.isKeyEntry(alias));
                            dto.setType(cert.getType());
                            dto.setSignatureAlgorithm(cert.getSigAlgName());
                            
                            dto.setKeyUsage(X509ExtensionInspector.extractKeyUsage(cert));
                            dto.setExtendedKeyUsage(X509ExtensionInspector.extractExtendedKeyUsage(cert));
                            dto.setCertificatePolicies(X509ExtensionInspector.extractCertificatePolicies(cert));
                            
                            certificates.add(dto);
                            
                            LOGGER.debug("Sertifika bulundu - Alias: {}, Serial: {}, Subject: {}", 
                                alias, dto.getSerialNumberHex(), dto.getSubject());
                        }
                    }
                } catch (Exception e) {
                    LOGGER.warn("Alias için sertifika bilgisi alınamadı: {} - {}", alias, e.getMessage());
                }
            }
            
            LOGGER.info("Toplam {} sertifika bulundu", certificates.size());
            
        } catch (Exception e) {
            throw new KeyStoreException("Sertifika listesi alınamadı: " + e.getMessage(), e);
        }
        
        return certificates;
    }

    /**
     * Sertifika bilgilerini konsol formatında yazdırır.
     * Command-line kullanımı için.
     */
    public void printCertificates(List<CertificateInfoDto> certificates) {
        if (certificates.isEmpty()) {
            System.out.println("\n⚠️  Keystore'da sertifika bulunamadı\n");
            return;
        }
        
        String separator = createSeparator(80, '=');
        String lineSeparator = createSeparator(80, '-');
        
        System.out.println("\n" + separator);
        System.out.println("🔐 KEYSTORE SERTİFİKALARI");
        System.out.println(separator);
        System.out.println();
        
        for (int i = 0; i < certificates.size(); i++) {
            CertificateInfoDto cert = certificates.get(i);
            
            System.out.println(String.format("📜 Sertifika #%d", i + 1));
            System.out.println(lineSeparator);
            System.out.println(String.format("  Alias:             %s", cert.getAlias()));
            System.out.println(String.format("  Serial (hex):      %s", cert.getSerialNumberHex()));
            System.out.println(String.format("  Serial (dec):      %s", cert.getSerialNumberDec()));
            System.out.println(String.format("  Subject:           %s", cert.getSubject()));
            System.out.println(String.format("  Issuer:            %s", cert.getIssuer()));
            System.out.println(String.format("  Valid From:        %s", cert.getValidFrom()));
            System.out.println(String.format("  Valid To:          %s", cert.getValidTo()));
            System.out.println(String.format("  Has Private Key:   %s", cert.isHasPrivateKey() ? "✅ Yes" : "❌ No"));
            System.out.println(String.format("  Type:              %s", cert.getType()));
            System.out.println(String.format("  Signature Algo:    %s", cert.getSignatureAlgorithm()));
            
            // OID bilgileri
            if (cert.getKeyUsage() != null && !cert.getKeyUsage().isEmpty()) {
                System.out.println(String.format("  Key Usage:         %s", cert.getKeyUsage()));
            }
            if (cert.getExtendedKeyUsage() != null && !cert.getExtendedKeyUsage().isEmpty()) {
                System.out.println(String.format("  Ext. Key Usage:    %s", cert.getExtendedKeyUsage()));
            }
            if (cert.getCertificatePolicies() != null && !cert.getCertificatePolicies().isEmpty()) {
                System.out.println(String.format("  Cert. Policies:    %s", cert.getCertificatePolicies()));
            }
            System.out.println();
        }
        
        System.out.println(separator);
        System.out.println(String.format("✅ Toplam %d sertifika bulundu\n", certificates.size()));
        
        // Environment variable örnekleri
        if (!certificates.isEmpty()) {
            CertificateInfoDto first = certificates.get(0);
            System.out.println("💡 Environment Variable Örnekleri:");
            System.out.println(lineSeparator);
            System.out.println(String.format("export CERTIFICATE_ALIAS=%s", first.getAlias()));
            System.out.println(String.format("export CERTIFICATE_SERIAL_NUMBER=%s", first.getSerialNumberHex()));
            System.out.println();
        }
    }

    /**
     * Java 8 uyumlu separator oluşturur (String.repeat() Java 11'de geldi).
     */
    private String createSeparator(int length, char character) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(character);
        }
        return sb.toString();
    }

}