IaikPkcs11Module.java
package io.mersel.dss.signer.api.services.keystore.iaik;
import eu.europa.esig.dss.enumerations.EncryptionAlgorithm;
import eu.europa.esig.dss.enumerations.SignatureAlgorithm;
import io.mersel.dss.signer.api.dtos.CertificateInfoDto;
import io.mersel.dss.signer.api.exceptions.KeyStoreException;
import io.mersel.dss.signer.api.services.X509ExtensionInspector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.StringUtils;
import org.xipki.pkcs11.wrapper.AttributeVector;
import org.xipki.pkcs11.wrapper.Mechanism;
import org.xipki.pkcs11.wrapper.PKCS11Constants;
import org.xipki.pkcs11.wrapper.PKCS11Exception;
import org.xipki.pkcs11.wrapper.PKCS11Module;
import org.xipki.pkcs11.wrapper.PKCS11Token;
import org.xipki.pkcs11.wrapper.Slot;
import org.xipki.pkcs11.wrapper.Token;
import org.xipki.pkcs11.wrapper.TokenException;
import org.xipki.pkcs11.wrapper.TokenInfo;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* xipki/ipkcs11wrapper (IAIK PKCS#11 Wrapper 1.6.8 kod tabanı) üzerinden HSM
* ile konuşan tek nokta. Spring container'da singleton bean olarak yaşar.
*
* <h2>Neden bu wrapper?</h2>
* <p>SunPKCS11 P11KeyStore, {@code CKF_LOGIN_REQUIRED} flag'i set olmayan
* tokenlerde (SafeNet ProtectServer K7, ProtectToolkit ve bazı Luna
* yapılandırmaları) alias map'i boş bırakıyor; sonuçta {@code keyStore.aliases()}
* sıfır entry döner ve hem listing hem imza akışı patlar. ipkcs11wrapper bu
* JCA-soyutlama katmanını tamamen by-pass eder; doğrudan PKCS#11 native
* köprüsünü kullanarak C_FindObjects / C_Sign çağırır.</p>
*
* <h2>Yaşam döngüsü</h2>
* <ol>
* <li>{@link InitializingBean#afterPropertiesSet()} → Module yükle, Slot
* seç, {@link PKCS11Token} oluştur (ctor login dahil).</li>
* <li>{@link #findSigner(String, String)} → alias veya serial üzerinden
* eşleşen private key + cert + zincir bulup {@link Pkcs11Signer} döndür.</li>
* <li>{@link #signOnSession(long, byte[], SignatureAlgorithm)} →
* (package-private) {@link IaikPkcs11Signer} bu metodu çağırır.</li>
* <li>{@link DisposableBean#destroy()} → token kapat (logout dahil), module
* finalize.</li>
* </ol>
*
* <h2>Thread-safety</h2>
* <p>{@link PKCS11Token} kendi içinde session pool ve lock yönetir; imzalama
* ({@link #signOnSession}) ve nesne okuma ({@link #collectAllRelevantObjects})
* akışları paralel yürür. Eş zamanlı imza üst sınırı
* {@link io.mersel.dss.signer.api.config.SignatureConfiguration} semaphore'u
* tarafından belirlenir. Yalnızca {@link #destroy()} ve cache yazma
* ({@link ConcurrentHashMap#computeIfAbsent}) koruma altında.</p>
*
* <h2>Acknowledgment</h2>
* <p>This product includes software developed by IAIK of Graz University of
* Technology.</p>
*/
public class IaikPkcs11Module implements InitializingBean, DisposableBean {
private static final Logger LOGGER = LoggerFactory.getLogger(IaikPkcs11Module.class);
private final String libraryPath;
private final Long slot;
private final Long slotIndex;
private final char[] pin;
/**
* Operatör elinde "bu kütüphane standart {@code C_Initialize(args)} kabul
* etmiyor" bilgisi varsa (TÜBİTAK BİLGEM AKİS macOS sürücüsünün klasik
* bug'ı) trial-and-error yapmadan doğrudan NULL-args yoluna gitmesini
* sağlar. Env var: {@code PKCS11_NULL_INIT_ARGS=true}.
*/
private final boolean forceNullInitArgs;
/**
* Per-thread {@link CertificateFactory}. {@code CertificateFactory} JDK
* specifikasyonunda <em>thread-safe garanti edilmiyor</em>: "Some
* implementations are thread-safe ... while others are not." SUN sağlayıcı
* X.509 pratikte güvenli çalışıyor ama production'da paralel cert listing
* altında nadir parse hatalarına açık kalmamak için ThreadLocal kullanıyoruz.
* Maliyet: thread başına bir kez factory.getInstance — ihmal edilebilir.
*/
private static final ThreadLocal<CertificateFactory> CERT_FACTORY =
ThreadLocal.withInitial(IaikPkcs11Module::createCertFactory);
private volatile PKCS11Module module;
private volatile PKCS11Token token;
/**
* PKCS#11 Cryptoki global state'ini <b>biz mi</b> initialize ettik —
* yoksa aynı process içindeki başka bir bileşen mi?
*
* <p>PKCS#11 spec'inde {@code C_Initialize} ve {@code C_Finalize}
* <b>process-global</b> state taşırlar; reference counting yoktur.
* {@code C_Finalize}'i kim çağırırsa, aynı kütüphane üzerinden çalışan
* tüm bileşenleri kapatır. Bu yüzden başka bir bileşen
* ({@code CKR_CRYPTOKI_ALREADY_INITIALIZED} ile karşılaştığımız
* senaryolarda) Cryptoki'yi init etmişse <b>biz finalize çağırmayız</b>;
* aksi halde paylaşımlı state'i agresifçe kapatıp diğer akışları
* patlatabiliriz (örn. uygulamada paralel çalışan başka bir HSM
* entegrasyonu, mock-CA test harness'i vb.).</p>
*
* <p>Yalnızca {@link #afterPropertiesSet()} içindeki
* {@code module.initialize()} çağrısı başarıyla geçtiğinde true
* olur; {@code CKR_CRYPTOKI_ALREADY_INITIALIZED} dönerse false kalır.</p>
*/
private volatile boolean ownsInitialization = false;
/**
* AKİS / TÜBİTAK uyumluluk yolu: {@code C_Initialize} {@code NULL} args ile
* yapıldıysa true. PKCS#11 v2.40 spec §5.4: "If pInitArgs is NULL_PTR,
* Cryptoki may not use threads." Bu yüzden bu modda {@link PKCS11Token}
* havuzunu <b>tek session</b>'a sıkıştırıyoruz — kütüphanenin paralel
* çağrı altında çökmesini ya da sessiz veri bozulmasını önlemek için.
* Akıllı kart donanımı zaten paralel oturum kaldırmıyor; bu kısıt gerçek
* kullanım için maliyetsiz.
*/
private volatile boolean singleThreadedMode = false;
/**
* İmza ataçımı için bulunan ilk private key + cert eşleşmesi cache'i.
* Concurrent: {@link ConcurrentHashMap#computeIfAbsent} ile lock-free okuma.
*/
private final Map<String, ResolvedKey> resolvedKeyCache = new ConcurrentHashMap<>();
public IaikPkcs11Module(String libraryPath, Long slot, Long slotIndex, char[] pin) {
this(libraryPath, slot, slotIndex, pin, false);
}
public IaikPkcs11Module(String libraryPath, Long slot, Long slotIndex, char[] pin,
boolean forceNullInitArgs) {
this.libraryPath = libraryPath;
this.slot = slot;
this.slotIndex = slotIndex;
this.pin = pin == null ? null : pin.clone();
this.forceNullInitArgs = forceNullInitArgs;
}
// --------------------------------------------------------------------
// Lifecycle
// --------------------------------------------------------------------
@Override
public void afterPropertiesSet() {
try {
LOGGER.info("ipkcs11wrapper modülü yükleniyor: library={}", libraryPath);
module = PKCS11Module.getInstance(libraryPath);
InitOutcome outcome = initializeIdempotent(module, forceNullInitArgs);
ownsInitialization = outcome.owned;
singleThreadedMode = outcome.singleThreaded;
Token rawToken = resolveToken();
TokenInfo info = rawToken.getTokenInfo();
LOGGER.info("Token açıldı: label='{}', manufacturer='{}', serial='{}'",
safeTrim(info.getLabel()),
safeTrim(info.getManufacturerID()),
safeTrim(info.getSerialNumber()));
// PKCS11Token ctor login'i kendisi yapar (pin verilirse). PIN
// sağlanmamışsa anonymous okuma denenir; CKA_PRIVATE=true objeleri
// okuma/imza patlar — operasyonel görünürlük için warn'leyelim.
char[] effectivePin = (pin != null && pin.length > 0) ? pin : null;
if (effectivePin == null) {
LOGGER.warn("PIN sağlanmadı; private key'lere erişim "
+ "CKA_PRIVATE=true objeler için başarısız olacak.");
}
// singleThreadedMode: PKCS#11 spec §5.4 NULL-init-args yolunda
// kütüphane thread güvencesi vermez → numSessions=1 ile pool'u
// sıkıştırırız; PKCS11Token kuyrukta seri kullandırır.
if (singleThreadedMode) {
LOGGER.info("AKİS uyumluluk modu aktif: PKCS11Token tek session ile "
+ "yaratılıyor (kütüphane paralel session güvencesi vermiyor).");
token = new PKCS11Token(rawToken, true /* readOnly */, effectivePin,
Integer.valueOf(1));
} else {
token = new PKCS11Token(rawToken, true /* readOnly */, effectivePin);
}
if (effectivePin != null) {
LOGGER.info("HSM token'a USER olarak login başarılı.");
}
} catch (Exception e) {
cleanupOnFailure();
throw new KeyStoreException(
"ipkcs11wrapper modülü initialize edilemedi: " + libraryPath, e);
}
}
@Override
public void destroy() {
synchronized (this) {
try {
if (token != null) {
try {
token.logout();
} catch (Exception e) {
LOGGER.debug("Logout sırasında (yoksayılan) hata: {}", e.getMessage());
}
try {
token.closeAllSessions();
} catch (Exception e) {
LOGGER.warn("Session close sırasında hata: {}", e.getMessage());
}
token = null;
}
if (module != null) {
if (ownsInitialization) {
try {
module.finalize(null);
LOGGER.debug("PKCS#11 modülü finalize edildi (ownership=us).");
} catch (Exception e) {
LOGGER.warn("Module finalize sırasında hata: {}", e.getMessage());
}
} else {
// CKR_CRYPTOKI_ALREADY_INITIALIZED ile gelen durumda
// başka bir bileşen Cryptoki state'i tutuyor; finalize
// çağırmak onların session/handle'larını da invalidate
// eder. Reference yok ⇒ kontrolü onlara bırak.
LOGGER.info("PKCS#11 modülü bizim tarafımızdan initialize "
+ "edilmediği için finalize çağrılmıyor (paylaşımlı Cryptoki "
+ "state korunur).");
}
module = null;
ownsInitialization = false;
singleThreadedMode = false;
}
if (pin != null) {
Arrays.fill(pin, '\0');
}
resolvedKeyCache.clear();
LOGGER.info("ipkcs11wrapper modülü kapatıldı.");
} catch (Exception e) {
LOGGER.warn("Modül kapatılırken hata: {}", e.getMessage());
}
}
}
// --------------------------------------------------------------------
// Public API — findSigner + listCertificates
// --------------------------------------------------------------------
/**
* Verilen alias veya serial numarasına eşleşen ilk imzalama materyalini
* döndürür. İkisi de boş bırakılırsa private key'i olan ilk sertifikayı
* seçer.
*
* @param alias CKA_LABEL ile eşleşme; {@code null}/boş ise dikkate alınmaz
* @param serialHex sertifika serial numarası (hex); {@code null}/boş ise dikkate alınmaz
* @return imza atmaya hazır {@link Pkcs11Signer}
* @throws KeyStoreException eşleşen anahtar yoksa
*/
/**
* Sertifika ya da private key handle'ı değişen (yeniden import, revoke,
* key rotation) durumlarda dahili cache'i sıfırlar. Operasyonel admin
* komutu / health endpoint'i bu metodu çağırabilir.
*
* <p>Cache aslında tek-shot startup-time için doluyor; uzun süren
* sunucularda HSM token'ında yeni sertifika eklenirse yeni
* {@code findSigner} çağrısı stale veri görmesin diye eklenmiştir.</p>
*/
public void invalidateKeyCache() {
int size = resolvedKeyCache.size();
resolvedKeyCache.clear();
LOGGER.info("ResolvedKey cache temizlendi (önceki entry sayısı: {}).", size);
}
public Pkcs11Signer findSigner(String alias, String serialHex) {
String cacheKey = (alias == null ? "" : alias) + "|" + (serialHex == null ? "" : serialHex);
// ConcurrentHashMap.computeIfAbsent → tek anahtar için tek arama;
// farklı alias talepleri paralel çözümlenir.
ResolvedKey resolved = resolvedKeyCache.computeIfAbsent(cacheKey,
k -> resolveFromToken(alias, serialHex));
return new IaikPkcs11Signer(this, resolved);
}
private ResolvedKey resolveFromToken(String alias, String serialHex) {
List<TokenObject> objects = collectAllRelevantObjects();
ResolvedKey resolved = matchKey(objects, alias, serialHex);
if (resolved == null) {
List<String> aliases = new ArrayList<>();
for (TokenObject obj : objects) {
if (obj.cert != null && obj.label != null) {
aliases.add(obj.label);
}
}
throw new KeyStoreException(
"Token'da eşleşen imzalama anahtarı bulunamadı"
+ (StringUtils.hasText(alias) ? " (alias='" + alias + "')" : "")
+ (StringUtils.hasText(serialHex) ? " (serial=" + serialHex + ")" : "")
+ ". Mevcut alias'lar: " + aliases);
}
LOGGER.info("İmzalama anahtarı çözüldü: alias='{}', serial={}, keyHandle=0x{}",
resolved.alias,
toHex(resolved.certificate.getSerialNumber()),
Long.toHexString(resolved.privateKeyHandle));
return resolved;
}
/**
* Token üzerindeki tüm sertifikaları (kombine private key bilgileriyle)
* listeler. SunPKCS11 alias map'ine bağımlı DEĞİLDİR — JCA katmanı boş
* dönse de buradan tam liste gelir.
*/
public List<CertificateInfoDto> listCertificates() {
List<TokenObject> objects = collectAllRelevantObjects();
Map<String, CertificateInfoDto> byAlias = new LinkedHashMap<>();
int orphanKeyCounter = 0;
for (TokenObject obj : objects) {
if (obj.cert == null) {
continue;
}
String serialHex = toHex(obj.cert.getSerialNumber());
String baseAlias = obj.label != null && !obj.label.isEmpty()
? obj.label
: ("cert-" + (byAlias.size() + 1));
// Duplicate label koruması (Codex regresyonu, Mayıs 2026): aynı
// CKA_LABEL'a sahip iki sertifika varsa map.put() birinciyi ezerdi.
// Operasyonel görünürlük için ikinciye serial-suffix ekliyoruz —
// kullanıcı listing'de iki entry görür ve hangisini istediğini
// serial ile seçebilir.
String alias = baseAlias;
if (byAlias.containsKey(alias)) {
String suffix = serialHex.length() > 8
? serialHex.substring(serialHex.length() - 8)
: serialHex;
alias = baseAlias + "@" + suffix;
LOGGER.warn("Duplicate alias '{}' tespit edildi; bu entry '{}' olarak listeleniyor.",
baseAlias, alias);
}
CertificateInfoDto dto = new CertificateInfoDto();
dto.setAlias(alias);
dto.setSerialNumberHex(serialHex);
dto.setSerialNumberDec(obj.cert.getSerialNumber().toString());
dto.setSubject(obj.cert.getSubjectX500Principal().toString());
dto.setIssuer(obj.cert.getIssuerX500Principal().toString());
dto.setValidFrom(obj.cert.getNotBefore());
dto.setValidTo(obj.cert.getNotAfter());
dto.setType(obj.cert.getType());
dto.setSignatureAlgorithm(obj.cert.getSigAlgName());
dto.setHasPrivateKey(hasMatchingKey(obj, objects));
dto.setKeyUsage(X509ExtensionInspector.extractKeyUsage(obj.cert));
dto.setExtendedKeyUsage(X509ExtensionInspector.extractExtendedKeyUsage(obj.cert));
dto.setCertificatePolicies(X509ExtensionInspector.extractCertificatePolicies(obj.cert));
byAlias.put(alias, dto);
}
// Yetim private key'leri (matching cert'i yok) ayrı entry olarak
// göster — operasyonel görünürlük için.
for (TokenObject obj : objects) {
if (obj.privateKeyHandle == 0L || obj.cert != null) {
continue;
}
if (matchingCertExists(obj, objects)) {
continue;
}
orphanKeyCounter++;
String alias = obj.label != null && !obj.label.isEmpty()
? obj.label
: ("orphan-key-" + orphanKeyCounter);
CertificateInfoDto dto = new CertificateInfoDto();
dto.setAlias(alias);
dto.setHasPrivateKey(true);
dto.setSubject("(yetim private key — token'da matching sertifika yok)");
byAlias.put(alias, dto);
}
LOGGER.info("Token listing: {} entry (cert+key={}, orphan-key={}).",
byAlias.size(), byAlias.size() - orphanKeyCounter, orphanKeyCounter);
return new ArrayList<>(byAlias.values());
}
// --------------------------------------------------------------------
// Package-private — IaikPkcs11Signer tarafından çağrılır
// --------------------------------------------------------------------
/**
* Sağlanan private key handle'ı üzerinden HSM'de imza atar.
*
* <p>{@link IaikSignatureMechanisms} ile DSS algoritmasını PKCS#11
* mekanizmasına çevirir; ECDSA için her zaman raw {@code CKM_ECDSA} +
* dış digest kullanır (universal HSM uyumu). RSA için combined
* {@code CKM_<HASH>_RSA_PKCS} ile tek round-trip imza atar; mekanizma
* reddedilirse {@link #signWithRawFallback} ile raw {@code CKM_RSA_PKCS} +
* PKCS#1 DigestInfo wrap'e düşer.</p>
*
* <p>PKCS11Token kendi içinde thread-safe oturum havuzu yönettiği için
* bu metoda eş zamanlı çağrı sağlanır; üst sınır
* {@code signatureSemaphore} ile kontrol edilir.</p>
*/
byte[] signOnSession(long privateKeyHandle,
byte[] dataToSign,
SignatureAlgorithm signatureAlgorithm) {
ensureTokenOpen();
Mechanism mechanism = IaikSignatureMechanisms.resolveMechanism(signatureAlgorithm);
byte[] inputData = IaikSignatureMechanisms.requiresExternalDigest(mechanism)
? hash(dataToSign, signatureAlgorithm)
: dataToSign;
try {
return invokeSign(mechanism, privateKeyHandle, inputData,
signatureAlgorithm, dataToSign.length);
} catch (PKCS11Exception ckEx) {
long errorCode = ckEx.getErrorCode();
// ─────────────────────────────────────────────────────────────
// Mekanizma uyumsuzluğu → raw fallback
// ─────────────────────────────────────────────────────────────
// ECDSA için {@link IaikSignatureMechanisms} zaten her zaman raw
// CKM_ECDSA kullanır — bu yüzden combined-mode mekanizma reddi
// pratik olarak yalnızca RSA tarafında oluşabilir. Yine de tüm
// standart "mekanizma desteklenmiyor" hata kodları için aynı
// savunmacı davranışı uyguluyoruz.
//
// RSA-PSS hariç (raw CKM_RSA_PKCS'e indirgeme PSS imzasını
// sessizce v1.5'e çevirir → yanlış imza; açıkça reddediyoruz).
// ─────────────────────────────────────────────────────────────
boolean mechanismRejected =
errorCode == PKCS11Constants.CKR_MECHANISM_INVALID
|| errorCode == PKCS11Constants.CKR_FUNCTION_NOT_SUPPORTED
|| errorCode == PKCS11Constants.CKR_KEY_TYPE_INCONSISTENT;
if (mechanismRejected) {
if (signatureAlgorithm.getEncryptionAlgorithm() == EncryptionAlgorithm.RSASSA_PSS) {
throw new io.mersel.dss.signer.api.exceptions.SignatureException(
"HSM bu sürümüyle RSA-PSS imzayı desteklemiyor (CKR=0x"
+ Long.toHexString(errorCode) + "); raw fallback PKCS#1 v1.5'e "
+ "indirgeme yapar, bu imza GEÇERSİZ olur. Lütfen RSA-PSS yerine "
+ "RSA-PKCS#1 v1.5 algoritmasıyla yapılandırın veya HSM "
+ "firmware'ini güncelleyin.", ckEx);
}
LOGGER.warn("Mekanizma reddedildi (CKR=0x{}, alg={}, mech=0x{}); "
+ "raw fallback'e düşülüyor.",
Long.toHexString(errorCode), signatureAlgorithm,
Long.toHexString(mechanism.getMechanismCode()));
return signWithRawFallback(privateKeyHandle, dataToSign, signatureAlgorithm);
}
throw new io.mersel.dss.signer.api.exceptions.SignatureException(
"HSM imza başarısız: " + ckEx.getMessage(), ckEx);
} catch (Exception e) {
throw new io.mersel.dss.signer.api.exceptions.SignatureException(
"HSM imza başarısız", e);
}
}
/**
* Tek bir {@code token.sign} çağrısını sarmalar (logging + EC/DSA
* normalize dahil). Doğrudan PKCS11Token'ın public API'sini kullanır;
* private internals'a reflection ile dokunmuyoruz.
*
* <h3>xipki/ipkcs11wrapper 1.0.9 opInit() swallow-bug</h3>
* <p>{@link PKCS11Token#sign(Mechanism, long, byte[])} kendi içinde
* {@code opInit()} çağırır; bu metot {@code C_SignInit} hatalarından SADECE
* {@code CKR_USER_NOT_LOGGED_IN}'i yakalar, diğer her
* {@code PKCS11Exception}'ı (CKR_MECHANISM_INVALID, CKR_KEY_HANDLE_INVALID,
* vs.) <em>sessizce yutar</em>; sonuç altta {@code CKR_OPERATION_NOT_INITIALIZED}
* olarak görünür ve gerçek hata kodu kaybolur.</p>
*
* <p>Bu bug'i deterministik tetiklemenin ana yolu (CI'da gözlenen) ECDSA
* combined mekanizmalarıydı ({@code CKM_ECDSA_SHA256} SoftHSM2'de
* mechanism-list'te var ama C_SignInit'te reddediliyor).
* {@link IaikSignatureMechanisms} ECDSA için her zaman raw {@code CKM_ECDSA}
* üreterek bu yolu by-pass eder. RSA combined mekanizmaları
* ({@code CKM_<HASH>_RSA_PKCS}) tüm production HSM'lerinde stabil
* çalışmıştır; bu yolda swallow-bug'a değme olasılığı pratik olarak yok.</p>
*
* <p>Upstream tracking: xipki/ipkcs11wrapper master {@code else throw ex}
* ile düzeltildi (commit 2024-11); resmi {@code v1.1+} sürümü yayınlandığında
* bu Javadoc notu kaldırılabilir.</p>
*/
private byte[] invokeSign(Mechanism mechanism,
long privateKeyHandle,
byte[] inputData,
SignatureAlgorithm signatureAlgorithm,
int originalDataLen) throws TokenException {
byte[] signature = token.sign(mechanism, privateKeyHandle, inputData);
LOGGER.debug("HSM imza tamamlandı: mech=0x{}, dataLen={}, sigLen={}",
Long.toHexString(mechanism.getMechanismCode()),
originalDataLen, signature.length);
return normalizeIfEcOrDsa(signature, signatureAlgorithm);
}
private byte[] signWithRawFallback(long privateKeyHandle,
byte[] dataToSign,
SignatureAlgorithm signatureAlgorithm) {
try {
EncryptionAlgorithm enc = signatureAlgorithm.getEncryptionAlgorithm();
// DSA için raw fallback yapmıyoruz: DSA'ya özel raw CKM_DSA
// mekanizması var ama IAIK fallback path'inde implement edilmedi.
// Yanlışlıkla CKM_RSA_PKCS'e indirgemek DSA private key ile
// anlamsız hatalar üretir (key/mechanism mismatch). DSA Türkiye
// e-imza pazarında pratik olarak ölü — explicit reject daha doğru.
if (enc == EncryptionAlgorithm.DSA) {
throw new io.mersel.dss.signer.api.exceptions.SignatureException(
"HSM bu sürümüyle CKM_DSA_<HASH> mekanizmasını desteklemiyor "
+ "ve DSA için raw fallback bu kod yolunda uygulanmıyor. "
+ "Lütfen RSA veya ECDSA anahtarı kullanın.");
}
Mechanism rawMech = (enc == EncryptionAlgorithm.ECDSA || enc == EncryptionAlgorithm.PLAIN_ECDSA)
? IaikSignatureMechanisms.fallbackToRawEcdsa()
: IaikSignatureMechanisms.fallbackToRawRsaPkcs();
byte[] inputData = hash(dataToSign, signatureAlgorithm);
if (enc == EncryptionAlgorithm.RSA) {
inputData = Pkcs1DigestInfo.wrap(inputData, signatureAlgorithm.getDigestAlgorithm());
}
// Raw mekanizma için de aynı invokeSign yolu kullanılır
// (token.sign + normalizeIfEcOrDsa). DRY: tek logging + normalize
// davranışı her iki kod yolunda da geçerlidir.
return invokeSign(rawMech, privateKeyHandle, inputData,
signatureAlgorithm, dataToSign.length);
} catch (io.mersel.dss.signer.api.exceptions.SignatureException e) {
throw e;
} catch (Exception e) {
throw new io.mersel.dss.signer.api.exceptions.SignatureException(
"Raw fallback imza da başarısız", e);
}
}
/**
* EC/DSA mekanizmalarında PKCS#11 spec'i imzayı <b>raw r||s</b> formatında
* verir; JCA / CMS / PAdES / XAdES ise ASN.1 DER SEQUENCE bekler. Bu
* dönüşüm yapılmazsa imza doğrulayıcılar tarafından reddedilir.
*
* <p>RSA imzaları (v1.5 ve PSS) zaten standardize encoded formatta gelir,
* dönüşüm gerekmez — bu metot RSA için imzayı olduğu gibi döndürür.</p>
*
* <p>Defensive: bazı HSM sürücüleri standart dışı davranıp zaten DER
* üretebilir; bu durumda {@link Pkcs11EcdsaSignatureEncoder#normalizeToDer}
* idempotent davranır ve yeniden sarmalamaz.</p>
*/
private static byte[] normalizeIfEcOrDsa(byte[] signature, SignatureAlgorithm signatureAlgorithm) {
EncryptionAlgorithm enc = signatureAlgorithm.getEncryptionAlgorithm();
if (enc == EncryptionAlgorithm.ECDSA
|| enc == EncryptionAlgorithm.PLAIN_ECDSA
|| enc == EncryptionAlgorithm.DSA) {
return Pkcs11EcdsaSignatureEncoder.normalizeToDer(signature);
}
return signature;
}
// --------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------
/** Token üzerindeki tüm cert ve private key objelerini handle + label + cert + id ile derle. */
private List<TokenObject> collectAllRelevantObjects() {
ensureTokenOpen();
List<TokenObject> result = new ArrayList<>();
try {
// 1) Tüm X.509 sertifikalar
long[] certHandles = token.findAllObjects(AttributeVector.newX509Certificate());
for (long handle : certHandles) {
AttributeVector av = token.getAttrValues(handle,
PKCS11Constants.CKA_LABEL,
PKCS11Constants.CKA_ID,
PKCS11Constants.CKA_VALUE);
TokenObject to = new TokenObject();
to.label = av.getStringAttrValue(PKCS11Constants.CKA_LABEL);
to.id = av.getByteArrayAttrValue(PKCS11Constants.CKA_ID);
to.cert = parseCert(av.getByteArrayAttrValue(PKCS11Constants.CKA_VALUE));
result.add(to);
}
// 2) Tüm private key'ler
long[] keyHandles = token.findAllObjects(AttributeVector.newPrivateKey());
for (long handle : keyHandles) {
AttributeVector av = token.getAttrValues(handle,
PKCS11Constants.CKA_LABEL,
PKCS11Constants.CKA_ID);
String label = av.getStringAttrValue(PKCS11Constants.CKA_LABEL);
byte[] id = av.getByteArrayAttrValue(PKCS11Constants.CKA_ID);
// Aynı cert ile eşleşen TokenObject zaten varsa private
// key handle'ını oraya iliştir; aksi halde key-only entry oluştur.
TokenObject match = findByIdOrLabel(result, id, label, true);
if (match != null) {
// Güvenlik (Codex regresyonu, Mayıs 2026): cert'e zaten
// bir private key bağlandıysa, ikinci bağlama girişimi
// duplicate label/id'yi gösterir — yanlış key'i seçmek
// yerine ilk bağlamayı koru ve loglayarak operatöre bildir.
if (match.privateKeyHandle != 0L) {
LOGGER.warn("Sertifika (CKA_LABEL='{}', CKA_ID={}) için birden fazla "
+ "private key adayı bulundu. İlk bağlama (handle=0x{}) korundu; "
+ "yeni handle=0x{} atlanıyor. HSM'de duplicate label/id "
+ "olabilir — admin kontrolü gerekir.",
match.label,
id == null ? "<empty>" : org.bouncycastle.util.encoders.Hex.toHexString(id),
Long.toHexString(match.privateKeyHandle),
Long.toHexString(handle));
// İlk bağlamayı koru, yeni handle'ı yetim key olarak da ekle.
TokenObject orphan = new TokenObject();
orphan.label = label;
orphan.id = id;
orphan.privateKeyHandle = handle;
result.add(orphan);
} else {
match.privateKeyHandle = handle;
}
} else {
TokenObject to = new TokenObject();
to.label = label;
to.id = id;
to.privateKeyHandle = handle;
result.add(to);
}
}
} catch (TokenException e) {
throw new KeyStoreException("Token nesneleri okunamadı", e);
}
return result;
}
private ResolvedKey matchKey(List<TokenObject> objects, String alias, String serialHex) {
TokenObject best = null;
for (TokenObject obj : objects) {
if (obj.cert == null || obj.privateKeyHandle == 0L) {
continue;
}
boolean aliasOk = !StringUtils.hasText(alias) || alias.equals(obj.label);
boolean serialOk = !StringUtils.hasText(serialHex)
|| serialHexEquals(obj.cert.getSerialNumber(), serialHex);
if (aliasOk && serialOk) {
best = obj;
break;
}
}
if (best == null) {
return null;
}
ResolvedKey rk = new ResolvedKey();
rk.alias = best.label != null ? best.label : toHex(best.cert.getSerialNumber());
rk.certificate = best.cert;
rk.certificateChain = Collections.singletonList(best.cert);
rk.privateKeyHandle = best.privateKeyHandle;
return rk;
}
private static boolean hasMatchingKey(TokenObject certObj, List<TokenObject> all) {
if (certObj.privateKeyHandle != 0L) {
return true;
}
TokenObject m = findByIdOrLabel(all, certObj.id, certObj.label, false);
return m != null && m.privateKeyHandle != 0L;
}
private static boolean matchingCertExists(TokenObject keyObj, List<TokenObject> all) {
TokenObject m = findByIdOrLabel(all, keyObj.id, keyObj.label, false);
return m != null && m.cert != null && m != keyObj;
}
/**
* Token üzerinde {@code cert ↔ private key} eşleştirmesi yapar.
*
* <h3>Güvenlik kontratı (Codex regresyonu, Mayıs 2026)</h3>
* <p>PKCS#11 standart pratiğinde {@code CKA_ID} bir keypair için benzersizdir
* (cert ve private key aynı ID'yi paylaşır). {@code CKA_LABEL} sadece
* human-readable bir isim olduğu için duplicate olabilir — özellikle key
* rotation, yanlış import, ya da admin hatası sonucu.</p>
*
* <p>Bu metod şu sıkı kuralları uygular:</p>
* <ol>
* <li><b>CKA_ID present</b> ise: yalnızca ID eşleşmesi geçerlidir.
* Label fallback YAPILMAZ — duplicate label durumunda yanlış
* eşleşme = yanlış imza riski var. Eşleşme yoksa {@code null} döner.</li>
* <li><b>CKA_ID absent</b> (her iki tarafta da boş) ise: label fallback
* yapılır AMA sadece tek-eşleşme garantili olmak şartıyla. Birden
* fazla aday varsa belirsizlik var demektir — {@code null} dönüp
* ambiguity'yi loglarız, asla rastgele bir aday seçmeyiz.</li>
* </ol>
*
* <p>Bu strict politika "sessiz yanlış imza" üretmek yerine "key not found"
* hatası verir; doğru davranış budur.</p>
*/
private static TokenObject findByIdOrLabel(List<TokenObject> objects,
byte[] id,
String label,
boolean preferCertSide) {
// (1) CKA_ID present → yalnızca ID eşleşmesi geçerli; label fallback yok.
if (id != null && id.length > 0) {
for (TokenObject obj : objects) {
if (obj.id != null && obj.id.length > 0
&& Arrays.equals(obj.id, id)
&& (!preferCertSide || obj.cert != null)) {
return obj;
}
}
// ID verilmiş ama eşleşme yok — burada label fallback'e
// dönmek key rotation senaryosunda yanlış key bağlar.
return null;
}
// (2) CKA_ID absent → label fallback'e tek-eşleşme garantili izin ver.
if (label != null && !label.isEmpty()) {
TokenObject only = null;
int matches = 0;
for (TokenObject obj : objects) {
if (!label.equals(obj.label)) continue;
if (preferCertSide && obj.cert == null) continue;
// Sadece kendisi de id-boş olan adaylarla eşle.
// (id'si dolu olanlar yukarıdaki yolla bağlanmalıdır.)
if (obj.id != null && obj.id.length > 0) continue;
only = obj;
if (++matches > 1) {
break;
}
}
if (matches == 1) {
return only;
}
if (matches > 1) {
LOGGER.warn("Belirsiz eşleşme: CKA_LABEL='{}' ve CKA_ID yok; "
+ "birden fazla aday var, hiçbiri seçilmiyor (sessiz yanlış "
+ "imza riskinden kaçınmak için).", label);
}
}
return null;
}
private void ensureTokenOpen() {
if (token == null) {
throw new KeyStoreException("ipkcs11wrapper modülü kapalı veya init edilmedi");
}
}
private Token resolveToken() throws PKCS11Exception {
Slot[] slots = module.getSlotList(true /* token present */);
if (slots.length == 0) {
throw new KeyStoreException("HSM'de hiç token bulunamadı (library=" + libraryPath + ")");
}
if (slot != null) {
for (Slot s : slots) {
if (s.getSlotID() == slot.longValue()) {
return s.getToken();
}
}
throw new KeyStoreException("PKCS11_SLOT=" + slot + " için token bulunamadı");
}
int idx = slotIndex != null ? slotIndex.intValue() : 0;
if (idx < 0 || idx >= slots.length) {
throw new KeyStoreException("PKCS11_SLOT_INDEX=" + idx
+ " sınır dışı; toplam slot=" + slots.length);
}
return slots[idx].getToken();
}
private void cleanupOnFailure() {
try { if (token != null) token.closeAllSessions(); } catch (Exception ignored) { }
// Sadece bizim initialize ettiğimiz Cryptoki'yi finalize et — paylaşımlı
// state korunmalı (destroy()'daki aynı ownership kuralı). Aksi halde
// init başarısızlığında, paylaşımlı kütüphaneyi de patlatırız.
if (module != null && ownsInitialization) {
try { module.finalize(null); } catch (Exception ignored) { }
}
token = null;
module = null;
ownsInitialization = false;
singleThreadedMode = false;
}
private static X509Certificate parseCert(byte[] der) {
if (der == null || der.length == 0) return null;
try {
return (X509Certificate) CERT_FACTORY.get()
.generateCertificate(new ByteArrayInputStream(der));
} catch (Exception e) {
LOGGER.warn("CKA_VALUE X.509 olarak parse edilemedi: {}", e.getMessage());
return null;
}
}
private static CertificateFactory createCertFactory() {
try {
return CertificateFactory.getInstance("X.509");
} catch (java.security.cert.CertificateException e) {
// JRE'de X.509 hep mevcuttur; buraya düşmek = bozuk JRE.
throw new IllegalStateException("X.509 CertificateFactory bulunamadı", e);
}
}
/**
* Aynı kütüphane bir başka bileşen tarafından önceden {@code C_Initialize}
* edilmişse SafeNet PSI-E3 gibi yapılar {@code CKR_CRYPTOKI_ALREADY_INITIALIZED}
* fırlatır. Bu durumu ölümcül kabul etmiyoruz — modülü zaten kullanabiliriz.
*
* <h3>AKİS uyumluluk fallback'i</h3>
* <p>TÜBİTAK BİLGEM'in {@code libakisp11.dylib} (macOS) ve bazı eski
* {@code libakisp11.so} (Linux) sürücüleri xipki'nin standart
* {@code C_Initialize(CK_C_INITIALIZE_ARGS{flags=CKF_OS_LOCKING_OK})}
* çağrısına {@code CKR_ARGUMENTS_BAD} döner — kütüphane macOS/Linux'ta
* yalnızca {@code C_Initialize(NULL)} formunu kabul ediyor (Windows
* portunda problem yok). Bu sürücü bug'ı için xipki'nin
* {@code PKCS11Module.initialize()} metodunu by-pass edip alttaki
* {@code PKCS11Implementation.C_Initialize(null, true)}'a doğrudan
* gidiyoruz; ardından {@code moduleInfo} ve {@code initVendor()}'ı
* reflection ile çalıştırıyoruz.</p>
*
* <p><b>Trade-off:</b> NULL args = PKCS#11 spec §5.4 gereği kütüphane
* thread-unsafe sayılır; {@link #afterPropertiesSet()} bu modu algılayıp
* {@link PKCS11Token} pool'unu {@code numSessions=1}'e indirir. Akıllı
* kart donanımı zaten paralel oturum kaldırmıyor → kullanıcı için
* görünür bir performans kaybı yok.</p>
*
* @return Sahiplik (finalize bizde mi?) ve mod (tek-thread mu?) bilgisi.
*/
private static InitOutcome initializeIdempotent(PKCS11Module module,
boolean forceNullInitArgs) throws PKCS11Exception {
if (forceNullInitArgs) {
LOGGER.info("PKCS11_NULL_INIT_ARGS=true → standart C_Initialize denenmeden "
+ "doğrudan NULL-args yoluna gidiliyor (AKİS / TÜBİTAK uyumluluk modu).");
initializeWithNullArgs(module);
return new InitOutcome(true, true);
}
try {
module.initialize();
return new InitOutcome(true, false);
} catch (PKCS11Exception e) {
if (e.getErrorCode() == PKCS11Constants.CKR_CRYPTOKI_ALREADY_INITIALIZED) {
LOGGER.info("PKCS#11 modülü önceden initialize edilmiş; mevcut state kullanılıyor "
+ "ve destroy() üzerinde finalize çağrılmayacak (paylaşımlı state korunur).");
return new InitOutcome(false, false);
}
if (e.getErrorCode() == PKCS11Constants.CKR_ARGUMENTS_BAD) {
// AKİS macOS sürücüsü tipik davranışı. Reflection ile NULL-args
// yoluna düş; başarılıysa singleThreadedMode aktive ederiz.
LOGGER.warn("Standart C_Initialize CKR_ARGUMENTS_BAD ile reddedildi "
+ "(genellikle TÜBİTAK AKİS macOS/Linux sürücüsü). NULL-args "
+ "fallback deneniyor (AKİS uyumluluk modu).");
initializeWithNullArgs(module);
LOGGER.info("NULL-args fallback başarılı; bundan sonra tek session "
+ "modunda çalışacağız (PKCS#11 spec §5.4).");
return new InitOutcome(true, true);
}
throw e;
}
}
/**
* xipki {@link PKCS11Module} private {@code pkcs11} alanını reflection ile
* tutuyoruz; alttaki IAIK {@code PKCS11Implementation.C_Initialize(null, true)}
* çağrısını doğrudan yapıyoruz. Ardından moduleInfo + vendor init adımlarını
* da reflection ile çalıştırıyoruz ki sonraki çağrılar (örn.
* {@code module.codeToName}) çökmeyelim.
*
* <p>İstisna yönetimi: moduleInfo / initVendor adımları best-effort —
* vendor.conf'ta AKİS girdisi yok, sorun değil; standart RSA/ECDSA
* mekanizmaları ipkcs11wrapper'da vendor-bağımsız çözülür.</p>
*/
private static void initializeWithNullArgs(PKCS11Module module) throws PKCS11Exception {
try {
Field pkcs11Field = PKCS11Module.class.getDeclaredField("pkcs11");
pkcs11Field.setAccessible(true);
Object pkcs11Impl = pkcs11Field.get(module);
if (pkcs11Impl == null) {
throw new IllegalStateException("PKCS11Module.pkcs11 reflection alanı null döndü; "
+ "ipkcs11wrapper sürümü beklenenden farklı olabilir.");
}
Method cInit = pkcs11Impl.getClass()
.getMethod("C_Initialize", Object.class, boolean.class);
try {
// Object[] cast explicit — varargs ambiguity'den kaçınmak için.
cInit.invoke(pkcs11Impl, new Object[] { null, Boolean.TRUE });
} catch (java.lang.reflect.InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof iaik.pkcs.pkcs11.wrapper.PKCS11Exception) {
iaik.pkcs.pkcs11.wrapper.PKCS11Exception iaikEx =
(iaik.pkcs.pkcs11.wrapper.PKCS11Exception) cause;
if (iaikEx.getErrorCode() == PKCS11Constants.CKR_CRYPTOKI_ALREADY_INITIALIZED) {
// Ortak bir bileşen Cryptoki state'i zaten tutuyor;
// outer initializeIdempotent ALREADY_INITIALIZED dalında
// ownership=false döndürmeli — biz buraya düştüysek
// demek ki ilk denemede başarılı olmadık ama başka biri
// initialize etmiş. Bu nadir bir yarış; konservatif:
// sahipliği reddet.
throw new PKCS11Exception(iaikEx.getErrorCode());
}
throw new PKCS11Exception(iaikEx.getErrorCode());
}
throw new IllegalStateException("Beklenmedik native C_Initialize hatası", cause);
}
// Best-effort: moduleInfo + initVendor.
try {
Method cGetInfo = pkcs11Impl.getClass().getMethod("C_GetInfo");
Object ckInfo = cGetInfo.invoke(pkcs11Impl);
Class<?> ckInfoClass = Class.forName("iaik.pkcs.pkcs11.wrapper.CK_INFO");
Class<?> moduleInfoClass = Class.forName("org.xipki.pkcs11.wrapper.ModuleInfo");
Object moduleInfo = moduleInfoClass
.getConstructor(ckInfoClass).newInstance(ckInfo);
Field moduleInfoField = PKCS11Module.class.getDeclaredField("moduleInfo");
moduleInfoField.setAccessible(true);
moduleInfoField.set(module, moduleInfo);
} catch (Exception ignored) {
LOGGER.debug("moduleInfo best-effort populate başarısız (kritik değil): {}",
ignored.getMessage());
}
try {
Method initVendor = PKCS11Module.class.getDeclaredMethod("initVendor");
initVendor.setAccessible(true);
initVendor.invoke(module);
} catch (Exception ignored) {
LOGGER.debug("initVendor best-effort çağrı başarısız (kritik değil): {}",
ignored.getMessage());
}
} catch (PKCS11Exception | RuntimeException ex) {
throw ex;
} catch (Exception ex) {
// Reflection / native köprüsünde beklenmedik hata → operatöre
// mesaj zincirini görünür tut.
throw new IllegalStateException(
"ipkcs11wrapper NULL-init-args fallback yolu başarısız: "
+ ex.getMessage(), ex);
}
}
/** {@link #initializeIdempotent} sonucu. Çağıran sınıf hangi yolu aldığımızı bilmeli. */
private static final class InitOutcome {
final boolean owned;
final boolean singleThreaded;
InitOutcome(boolean owned, boolean singleThreaded) {
this.owned = owned;
this.singleThreaded = singleThreaded;
}
}
private static String safeTrim(Object value) {
if (value == null) return "";
if (value instanceof char[]) {
return new String((char[]) value).trim();
}
return value.toString().trim();
}
private static String toHex(BigInteger n) {
return n == null ? "" : n.toString(16).toUpperCase();
}
private static boolean serialHexEquals(BigInteger candidate, String requested) {
try {
return Objects.equals(candidate, new BigInteger(requested, 16));
} catch (Exception e) {
return false;
}
}
private static byte[] hash(byte[] data, SignatureAlgorithm sa) {
try {
String algo = sa.getDigestAlgorithm().getJavaName();
return MessageDigest.getInstance(algo).digest(data);
} catch (Exception e) {
throw new io.mersel.dss.signer.api.exceptions.SignatureException(
"Digest hesaplanamadı: " + sa, e);
}
}
// --------------------------------------------------------------------
// Data classes
// --------------------------------------------------------------------
/** Token üzerinde bulunan tek bir obje (cert, private key veya her ikisi). */
private static final class TokenObject {
String label;
byte[] id;
X509Certificate cert;
long privateKeyHandle; // 0 = yok
}
/**
* {@link #findSigner} sonucu: cert + chain + key handle. {@link IaikPkcs11Signer}
* bu yapıyı tutar ve sign çağrısında handle'ı modüle delege eder.
*/
static final class ResolvedKey {
String alias;
X509Certificate certificate;
List<X509Certificate> certificateChain;
long privateKeyHandle;
}
}