SignatureConfiguration.java

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

import eu.europa.esig.dss.service.crl.OnlineCRLSource;
import eu.europa.esig.dss.service.ocsp.OnlineOCSPSource;
import eu.europa.esig.dss.spi.validation.CertificateVerifier;
import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier;
import eu.europa.esig.dss.spi.validation.RevocationDataVerifier;
import eu.europa.esig.dss.spi.x509.CommonTrustedCertificateSource;
import eu.europa.esig.dss.spi.x509.aia.DefaultAIASource;
import eu.europa.esig.dss.xades.signature.XAdESSigningTimeZoneHolder;
import io.mersel.dss.signer.api.models.SigningContext;
import io.mersel.dss.signer.api.models.SigningMaterial;
import io.mersel.dss.signer.api.models.configurations.SignatureServiceConfiguration;
import io.mersel.dss.signer.api.services.SigningMaterialFactory;
import io.mersel.dss.signer.api.services.certificate.CertificateChainProvider;
import io.mersel.dss.signer.api.services.certificate.LocalCertificateChainProvider;
import io.mersel.dss.signer.api.services.certificate.OnlineCertificateChainProvider;
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.PfxKeyStoreProvider;
import io.mersel.dss.signer.api.services.keystore.iaik.IaikPkcs11Module;
import io.mersel.dss.signer.api.services.KamusmRootCertificateService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.security.cert.X509Certificate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;

/**
 * İmza servisleri için ana yapılandırma.
 * Elektronik imza işlemleri için gereken tüm bean'leri ayarlar.
 */
@Configuration
public class SignatureConfiguration {

    private final SignatureServiceConfiguration config;
    private final KamusmRootCertificateService rootCertificateService;

    private static final org.slf4j.Logger LOGGER =
            org.slf4j.LoggerFactory.getLogger(SignatureConfiguration.class);

    public SignatureConfiguration(SignatureServiceConfiguration config,
                                 KamusmRootCertificateService rootCertificateService) {
        this.config = config;
        this.rootCertificateService = rootCertificateService;
        
        // DSS OCSP için GET metodu kullanımını etkinleştir
        System.setProperty("dss.http.use.get.for.ocsp", "true");

        configureXadesSigningTimeZone();
    }

    /**
     * XAdES {@code <SigningTime>} timezone'unu Spring config'ten okuyup DSS
     * override builder'ın gördüğü statik holder'a yansıtır. Spring container
     * dışında çalışan testlerde holder default'ta ({@code +03:00}) kalır.
     *
     * <p>Geçersiz timezone string'i fail-fast davranır: container açılışta
     * patlar, üretimde sessizce yanlış formatta imza üretmek yerine erken
     * hata.</p>
     */
    private void configureXadesSigningTimeZone() {
        ZoneId zone = config.getXadesSigningTimeZone();
        XAdESSigningTimeZoneHolder.setZone(zone);
        LOGGER.info("XAdES SigningTime timezone yapılandırıldı: {} (raw='{}')",
                zone, config.getXadesSigningTimeZoneRaw());
    }

    /**
     * KeyStore sağlayıcısı bean'i.
     *
     * <p>Konfigürasyon tarafından hangi yol seçilirse o sağlayıcı üretilir:
     * <ul>
     *   <li>PKCS#11 ({@code PKCS11_LIBRARY}) → {@link PKCS11KeyStoreProvider}.
     *       Bu sağlayıcı artık <b>sadece listing'in eski JCA fallback yolu</b>
     *       için var; gerçek imzalama akışı {@link IaikPkcs11Module} üzerinden
     *       yürütülür.</li>
     *   <li>PFX ({@code PFX_PATH}) → {@link PfxKeyStoreProvider}.</li>
     * </ul>
     * </p>
     */
    @Bean
    public KeyStoreProvider keyStoreProvider() {
        if (StringUtils.hasText(config.getPkcs11LibraryPath())) {
            return new PKCS11KeyStoreProvider(
                config.getPkcs11LibraryPath(),
                config.getPkcs11Slot(),
                config.getPkcs11SlotIndex()
            );
        }

        if (StringUtils.hasText(config.getPfxPath())) {
            return new PfxKeyStoreProvider(config.getPfxPath());
        }

        throw new IllegalStateException(
            "Ne PKCS11_LIBRARY ne de PFX_PATH yapılandırılmamış. " +
            "En az bir keystore kaynağı belirtilmelidir.");
    }

    /**
     * IAIK PKCS#11 modülü — yalnızca {@code PKCS11_LIBRARY} property'si
     * <b>boş olmayan</b> bir değer içeriyorsa Spring container'da bean
     * olarak yaratılır.
     *
     * <p><b>Neden {@link ConditionalOnExpression}?</b> Spring Boot'un
     * {@code @ConditionalOnProperty(name = "X")} davranışı: property
     * <em>tanımlı</em> ve değeri {@code "false"} değilse bean'i aktive
     * eder. Yani {@code PKCS11_LIBRARY=} (boş string) ya da
     * {@code PKCS11_LIBRARY=   } (whitespace) durumunda bean aktive olur
     * ve {@link IaikPkcs11Module} boş library path ile startup'ta patlar.</p>
     *
     * <p>{@code StringUtils.hasText} ile null + boş + whitespace
     * vakalarının üçü de elenir. Bu, container ortamlarında env var'ı
     * boş bırakma (örn. {@code -e PKCS11_LIBRARY=}) hatasının erken
     * yakalanmasını sağlar.</p>
     *
     * <p>Inisializasyon sırasında native kütüphane yüklenir, token açılır ve
     * USER ile login olunur. Spring yaşam döngüsünden çıkarken
     * {@code destroyMethod="destroy"} otomatik çağrılır ve token temiz
     * kapatılır.</p>
     *
     * <p>PFX yapılandırmasında bu bean container'da YOK demektir; aşağıdaki
     * {@link #signingMaterial} bean'i {@link ObjectProvider} üzerinden
     * yokluğu algılar ve PFX yoluna düşer.</p>
     */
    @Bean(destroyMethod = "destroy")
    @ConditionalOnExpression("#{T(org.springframework.util.StringUtils).hasText('${PKCS11_LIBRARY:}')}")
    public IaikPkcs11Module iaikPkcs11Module() {
        char[] pin = config.getCertificatePin().toCharArray();
        Long slot = sanitizeSlotConfig(config.getPkcs11Slot());
        Long slotIndex = sanitizeSlotConfig(config.getPkcs11SlotIndex());
        return new IaikPkcs11Module(
            config.getPkcs11LibraryPath(),
            slot,
            slotIndex,
            pin,
            config.isPkcs11NullInitArgs());
    }

    /** {@code PKCS11_SLOT:-1} default convention'ını {@code null}'a normalize eder. */
    private static Long sanitizeSlotConfig(Long raw) {
        return raw != null && raw >= 0 ? raw : null;
    }

    /**
     * Öncelik sırasına göre sertifika zinciri sağlayıcılarını verir.
     */
    @Bean
    public List<CertificateChainProvider> certificateChainProviders() {
        List<CertificateChainProvider> providers = new ArrayList<>();
        
        // Online sağlayıcı (yüksek öncelik)
        if (config.isCertificateChainGetOnline()) {
            providers.add(new OnlineCertificateChainProvider());
        }
        
        // Yerel dosya sağlayıcı (yedek)
        if (StringUtils.hasText(config.getIssuerCertificatePath()) || 
            StringUtils.hasText(config.getCaCertificatePath())) {
            providers.add(new LocalCertificateChainProvider(
                config.getIssuerCertificatePath(),
                config.getCaCertificatePath()
            ));
        }
        
        return providers;
    }

    /**
     * Singleton {@link SigningContext} — başlangıçta bir kez çözümlenir.
     *
     * <p>Backend seçimi: container'da {@link IaikPkcs11Module} bean'i varsa
     * HSM yolu, yoksa PFX yolu kullanılır.
     * {@link ObjectProvider#getIfAvailable()} bean yokluğunu temiz şekilde
     * algılamamızı sağlar.</p>
     */
    @Bean
    public SigningContext signingContext(SigningMaterialFactory factory,
                                         KeyStoreProvider keyStoreProvider,
                                         ObjectProvider<IaikPkcs11Module> iaikModuleProvider) {
        IaikPkcs11Module iaikModule = iaikModuleProvider.getIfAvailable();
        if (iaikModule != null) {
            return factory.createPkcs11SigningContext(
                iaikModule,
                config.getCertificateAlias(),
                config.getCertificateSerialNumber());
        }

        char[] pin = config.getCertificatePin().toCharArray();
        return factory.createPfxSigningContext(
            keyStoreProvider,
            pin,
            config.getCertificateAlias(),
            config.getCertificateSerialNumber());
    }

    /**
     * Uygulama için ana imzalama materyalini sağlar.
     * Başlangıçta bir kez oluşturulur ve tüm imzalama işlemleri için tekrar kullanılır.
     */
    @Bean
    public SigningMaterial signingMaterial(SigningContext signingContext) {
        return signingContext.getMaterial();
    }

    /**
     * Keystore işlemleri için imzalama alias'ını sağlar.
     */
    @Bean
    public String signingAlias(SigningContext signingContext) {
        return signingContext.getAlias();
    }

    /**
     * İmzalama PIN'ini char dizisi olarak sağlar.
     */
    @Bean
    public char[] signingPin() {
        return config.getCertificatePin().toCharArray();
    }

    /**
     * Güvenilir kök sertifikalar listesini sağlar.
     */
    @Bean
    public List<X509Certificate> trustedRootCertificates() {
        return rootCertificateService.getTrustedRoots();
    }

    /**
     * Eşzamanlı imza işlemlerini kontrol etmek için semaphore sağlar.
     */
    @Bean
    public Semaphore signatureSemaphore() {
        return new Semaphore(config.getMaxSessionCount());
    }

    /**
     * Tam yapılandırılmış DSS sertifika doğrulayıcısını sağlar.
     */
    @Bean
    public CertificateVerifier certificateVerifier() {
        CommonCertificateVerifier verifier = new CommonCertificateVerifier();
        
        // Güvenilir sertifika kaynaklarını yapılandır
        CommonTrustedCertificateSource trustedSource = new CommonTrustedCertificateSource();
        rootCertificateService.getTrustedRootTokens()
            .forEach(trustedSource::addCertificate);
        verifier.setTrustedCertSources(trustedSource);

        // Güvenilmeyen zincirler için iptal kontrolünü etkinleştir
        verifier.setCheckRevocationForUntrustedChains(true);

        // İptal verisi doğrulayıcısını yapılandır
        RevocationDataVerifier revocationVerifier = 
            RevocationDataVerifier.createDefaultRevocationDataVerifier();
        Long fiveMinutes = 5 * 60 * 1000L;
        revocationVerifier.setCheckRevocationFreshnessNextUpdate(true);
        revocationVerifier.setSignatureMaximumRevocationFreshness(fiveMinutes);
        revocationVerifier.setTimestampMaximumRevocationFreshness(fiveMinutes);
        revocationVerifier.setRevocationMaximumRevocationFreshness(fiveMinutes);
        verifier.setRevocationDataVerifier(revocationVerifier);

        // İptal yedeklemeyi etkinleştir
        verifier.setRevocationFallback(true);

        // OCSP kaynağını yapılandır
        OnlineOCSPSource ocspSource = new OnlineOCSPSource();
        verifier.setOcspSource(ocspSource);

        // Zincir oluşturma için AIA kaynağını yapılandır
        DefaultAIASource aiaSource = new DefaultAIASource();
        verifier.setAIASource(aiaSource);

        // CRL kaynağını yapılandır
        OnlineCRLSource crlSource = new OnlineCRLSource();
        verifier.setCrlSource(crlSource);

        return verifier;
    }

    /**
     * Sertifika doğrulayıcı ile yapılandırılmış XAdES servisini sağlar.
     */
    @Bean
    public eu.europa.esig.dss.xades.signature.XAdESService xadesService(CertificateVerifier certificateVerifier) {
        return new eu.europa.esig.dss.xades.signature.XAdESService(certificateVerifier);
    }

    /**
     * Sertifika doğrulayıcı ile yapılandırılmış CAdES servisini sağlar.
     */
    @Bean
    public eu.europa.esig.dss.cades.signature.CAdESService cadesService(CertificateVerifier certificateVerifier) {
        return new eu.europa.esig.dss.cades.signature.CAdESService(certificateVerifier);
    }
}