KeyStoreLoaderService.java
package io.mersel.dss.signer.api.services.keystore;
import io.mersel.dss.signer.api.exceptions.KeyStoreException;
import io.mersel.dss.signer.api.models.SigningKeyEntry;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigInteger;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
/**
* PFX / PKCS#12 yolu için keystore yükleme ve imzalama anahtarı çözümleme.
*
* <p>Bu servis artık <b>sadece JCA tabanlı keystore'lar</b> (PFX dosyaları)
* için çalışır. PKCS#11 (HSM) yolu tamamen
* {@link io.mersel.dss.signer.api.services.keystore.iaik.IaikPkcs11Module}
* üzerinden yürütülür ve bu servisten geçmez.</p>
*/
@Service
public class KeyStoreLoaderService {
private static final Logger LOGGER = LoggerFactory.getLogger(KeyStoreLoaderService.class);
/** Sağlayıcı üzerinden JCA KeyStore yükler. */
public KeyStore loadKeyStore(KeyStoreProvider provider, char[] pin) {
return provider.loadKeyStore(pin);
}
/**
* Backward-compat — tek metod ismi, provider parametresi olmadan da çalışır.
*/
public SigningKeyEntry resolveKeyEntry(KeyStore keyStore,
char[] pin,
String certificateAlias,
String certificateSerialNumber) {
return resolveKeyEntry(keyStore, null, pin, certificateAlias, certificateSerialNumber);
}
/**
* Alias veya seri numarasına göre keystore'dan imzalama anahtar girdisini çözümler.
*
* <p>Alias verilmişse doğrudan {@code keyStore.getEntry(alias, ...)}; serial
* verilmişse {@code keyStore.aliases()} üzerinde döner ve serial eşleşeni
* bulur. İkisi de verilmemişse private key'i olan ilk entry seçilir.</p>
*
* @param keyStore Yüklenmiş PFX KeyStore
* @param provider {@link KeyStoreProvider} — sadece hata mesajları için referans, listing yapmaz
* @param pin Private key'lere erişim için PIN
* @param certificateAlias İsteğe bağlı alias
* @param certificateSerialNumber İsteğe bağlı serial (hex)
*/
public SigningKeyEntry resolveKeyEntry(KeyStore keyStore,
KeyStoreProvider provider,
char[] pin,
String certificateAlias,
String certificateSerialNumber) {
try {
KeyStore.PasswordProtection protection = new KeyStore.PasswordProtection(pin);
ensureBouncyCastleRegistered();
if (StringUtils.hasText(certificateAlias)) {
return resolveByAlias(keyStore, certificateAlias, protection);
}
return resolveByIteration(keyStore, certificateSerialNumber, protection);
} catch (KeyStoreException e) {
throw e;
} catch (Exception e) {
throw new KeyStoreException("Keystore'dan imzalama anahtarı çözümlenemedi", e);
}
}
private SigningKeyEntry resolveByAlias(KeyStore keyStore,
String alias,
KeyStore.PasswordProtection protection) throws Exception {
if (!keyStore.isKeyEntry(alias)) {
throw new KeyStoreException(
"Alias '" + alias + "' KeyStore'da key entry değil. "
+ "Mevcut alias'lar: " + listAliasesSafe(keyStore));
}
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, protection);
if (entry == null) {
throw new KeyStoreException("Alias '" + alias + "' için getEntry() null döndü");
}
LOGGER.info("İmzalama anahtarı bulundu: alias='{}'", alias);
return new SigningKeyEntry(alias, entry);
}
private SigningKeyEntry resolveByIteration(KeyStore keyStore,
String requestedSerial,
KeyStore.PasswordProtection protection) throws Exception {
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
try {
if (!keyStore.isKeyEntry(alias)) {
continue;
}
if (StringUtils.hasText(requestedSerial)) {
java.security.cert.Certificate c = keyStore.getCertificate(alias);
if (!(c instanceof X509Certificate)) {
continue;
}
if (!serialHexEquals(((X509Certificate) c).getSerialNumber(), requestedSerial)) {
continue;
}
}
KeyStore.PrivateKeyEntry entry =
(KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, protection);
if (entry != null) {
LOGGER.info("İmzalama anahtarı bulundu: alias='{}'", alias);
return new SigningKeyEntry(alias, entry);
}
} catch (Exception e) {
LOGGER.warn("Alias '{}' incelenirken hata: {}", alias, e.getMessage());
}
}
throw new KeyStoreException(
"KeyStore'da uygun imzalama anahtarı bulunamadı"
+ (StringUtils.hasText(requestedSerial) ? " (serial=" + requestedSerial + ")" : "")
+ ". Mevcut alias'lar: " + listAliasesSafe(keyStore));
}
private static boolean serialHexEquals(BigInteger candidate, String requestedHex) {
try {
return candidate != null && candidate.equals(new BigInteger(requestedHex, 16));
} catch (NumberFormatException e) {
return false;
}
}
private static List<String> listAliasesSafe(KeyStore keyStore) {
List<String> result = new ArrayList<>();
try {
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
result.add(aliases.nextElement());
}
} catch (Exception ignored) {
// boş liste OK
}
return result;
}
/**
* PFX yolunda BouncyCastle JCA provider'ını garanti eder. PFX dosyalarındaki
* EC anahtarları SunEC'nin parse edemediği explicit ECParameters içerebilir;
* BC eklenince hem named hem explicit form parse edilir.
*
* <h3>⚠️ JVM-wide yan etki</h3>
* <p>Bu metod {@code Security.removeProvider("SunEC")} + BouncyCastle'ı
* {@code position=1} olarak ekler. Bu değişiklik <b>process'in tüm JCA
* akışını etkiler</b>: TLS handshake, DSS validation, başka bir bean'in
* {@code Signature.getInstance}'ı vs. Migration öncesi davranış — yeni
* bir yan etki değil. Yine de farkındalık:</p>
* <ul>
* <li>İlk PFX yükleme anında bir kez tetiklenir, sonra idempotent.</li>
* <li>HSM/PKCS#11 yolunda bu metoda <b>hiç</b> uğranılmaz — JVM provider
* sırası bozulmaz.</li>
* <li>Eğer ileride başka bir bileşen FIPS-only veya SunEC-specific
* davranışa bağlı çalışırsa, PFX yolunu kullanıma soktuğunuzda
* beklenmedik regression görebilirsiniz.</li>
* </ul>
*/
private static synchronized void ensureBouncyCastleRegistered() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null
&& Security.getProvider("SunEC") == null) {
return;
}
Security.removeProvider("SunEC");
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
}
LOGGER.info("BouncyCastle EC AlgorithmParameters desteği etkinleştirildi, SunEC kaldırıldı");
}
}