TimestampService.java
package io.mersel.dss.signer.api.services.timestamp;
import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import eu.europa.esig.dss.enumerations.EncryptionAlgorithm;
import eu.europa.esig.dss.enumerations.SignatureAlgorithm;
import eu.europa.esig.dss.model.TimestampBinary;
import eu.europa.esig.dss.service.tsp.OnlineTSPSource;
import io.mersel.dss.signer.api.dtos.TimestampRequestDto;
import io.mersel.dss.signer.api.dtos.TimestampResponseDto;
import io.mersel.dss.signer.api.dtos.TimestampValidationDto;
import io.mersel.dss.signer.api.dtos.TimestampValidationResponseDto;
import io.mersel.dss.signer.api.exceptions.TimestampException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.tsp.TimeStampResponse;
import org.bouncycastle.util.Store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Zaman damgası (timestamp) işlemleri için servis.
* RFC 3161 standardına uygun TSQ (Time Stamp Query), TSR (Time Stamp Response)
* ve validasyon işlemlerini gerçekleştirir.
*/
@Service
public class TimestampService {
private static final Logger LOGGER = LoggerFactory.getLogger(TimestampService.class);
private static final SimpleDateFormat ISO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static {
ISO_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private final TimestampConfigurationService timestampConfigurationService;
public TimestampService(TimestampConfigurationService timestampConfigurationService) {
this.timestampConfigurationService = timestampConfigurationService;
}
/**
* Binary belge için zaman damgası alır (DTO versiyonu - geriye dönük uyumluluk için).
*
* @param requestDto Zaman damgası talebi
* @return Zaman damgası yanıtı
* @throws TimestampException Zaman damgası alınamadığında
*/
public TimestampResponseDto getTimestamp(TimestampRequestDto requestDto) {
try {
LOGGER.info("Zaman damgası talebi alındı. Hash algoritması: {}", requestDto.getHashAlgorithm());
// Base64 encoded veriyi decode et
byte[] documentData = Base64.getDecoder().decode(requestDto.getDocumentData());
return getTimestamp(documentData, requestDto.getHashAlgorithm());
} catch (Exception e) {
LOGGER.error("Zaman damgası alınırken hata oluştu", e);
throw new TimestampException("Zaman damgası alınamadı: " + e.getMessage(), e);
}
}
/**
* Binary belge için zaman damgası alır.
*
* @param documentData Belge verisi
* @param hashAlgorithm Hash algoritması (null ise SHA256 kullanılır)
* @return Zaman damgası yanıtı
* @throws TimestampException Zaman damgası alınamadığında
*/
public TimestampResponseDto getTimestamp(byte[] documentData, String hashAlgorithm) {
try {
LOGGER.info("Zaman damgası talebi alındı. Hash algoritması: {}", hashAlgorithm);
// Hash algoritmasını belirle
DigestAlgorithm digestAlgorithm = getDigestAlgorithm(hashAlgorithm);
// Belgenin hash'ini hesapla
byte[] digest = computeDigest(documentData, digestAlgorithm);
// TSP source'u al
OnlineTSPSource tspSource = timestampConfigurationService.getTspSource();
// DSS ile timestamp al
TimestampBinary timestampBinary = tspSource.getTimeStampResponse(digestAlgorithm, digest);
byte[] timestampBytes = timestampBinary.getBytes();
// DSS'den gelen TimestampToken'ı kullan
eu.europa.esig.dss.spi.x509.tsp.TimestampToken dssToken = new eu.europa.esig.dss.spi.x509.tsp.TimestampToken(
timestampBytes, eu.europa.esig.dss.enumerations.TimestampType.CONTENT_TIMESTAMP);
// Response DTO'yu oluştur
TimestampResponseDto response = new TimestampResponseDto();
response.setTimestampToken(Base64.getEncoder().encodeToString(timestampBytes));
response.setTimestamp(ISO_DATE_FORMAT.format(dssToken.getGenerationTime()));
response.setHashAlgorithm(digestAlgorithm.getName());
response.setSerialNumber(dssToken.getDSSIdAsString());
// TSA bilgisini al
if (dssToken.getIssuerX500Principal() != null) {
response.setTsaName(dssToken.getIssuerX500Principal().getName());
}
LOGGER.info("Zaman damgası başarıyla alındı. Tarih: {}", response.getTimestamp());
return response;
} catch (Exception e) {
LOGGER.error("Zaman damgası alınırken hata oluştu", e);
throw new TimestampException("Zaman damgası alınamadı: " + e.getMessage(), e);
}
}
/**
* Zaman damgasını doğrular (DTO versiyonu - geriye dönük uyumluluk için).
*
* @param validationDto Doğrulama talebi
* @return Doğrulama sonucu
*/
public TimestampValidationResponseDto validateTimestamp(TimestampValidationDto validationDto) {
try {
// Timestamp token'ı decode et
byte[] timestampBytes = Base64.getDecoder().decode(validationDto.getTimestampToken());
// Orijinal veri varsa decode et
byte[] originalData = null;
if (StringUtils.hasText(validationDto.getOriginalData())) {
originalData = Base64.getDecoder().decode(validationDto.getOriginalData());
}
return validateTimestamp(timestampBytes, originalData);
} catch (Exception e) {
LOGGER.error("Zaman damgası doğrulama hatası", e);
TimestampValidationResponseDto response = new TimestampValidationResponseDto();
response.setValid(false);
response.setErrors(Arrays.asList("Genel doğrulama hatası: " + e.getMessage()));
response.setMessage("Zaman damgası doğrulanamadı");
return response;
}
}
/**
* Zaman damgasını doğrular.
*
* @param timestampBytes Timestamp token bytes
* @param originalData Orijinal belge (opsiyonel, hash doğrulaması için)
* @return Doğrulama sonucu
*/
public TimestampValidationResponseDto validateTimestamp(byte[] timestampBytes, byte[] originalData) {
TimestampValidationResponseDto response = new TimestampValidationResponseDto();
List<String> errors = new ArrayList<>();
try {
LOGGER.info("Zaman damgası doğrulama talebi alındı. Token boyutu: {} bytes", timestampBytes.length);
// BouncyCastle token'ı parse et
org.bouncycastle.tsp.TimeStampToken bcToken = parseBCTimestampToken(timestampBytes);
if (bcToken == null) {
errors.add("Timestamp token parse edilemedi");
response.setValid(false);
response.setErrors(errors);
response.setMessage("Zaman damgası token'ı geçersiz format");
return response;
}
// Temel bilgileri doldur (BouncyCastle token'dan)
response.setTimestamp(ISO_DATE_FORMAT.format(bcToken.getTimeStampInfo().getGenTime()));
// Hash algoritmasını hem isim hem OID olarak set et
String hashAlgOid = bcToken.getTimeStampInfo().getHashAlgorithm().getAlgorithm().getId();
response.setHashAlgorithmOid(hashAlgOid);
try {
DigestAlgorithm digestAlg = DigestAlgorithm.forOID(hashAlgOid);
response.setHashAlgorithm(digestAlg.getName());
} catch (Exception e) {
LOGGER.debug("Hash algoritması DSS'de bulunamadı, OID kullanılıyor: {}", hashAlgOid);
response.setHashAlgorithm(hashAlgOid);
}
response.setSerialNumber(bcToken.getTimeStampInfo().getSerialNumber().toString());
// İmza algoritmasını hem isim hem OID olarak set et
try {
CMSSignedData signedData = bcToken.toCMSSignedData();
if (signedData.getSignerInfos().size() > 0) {
org.bouncycastle.cms.SignerInformation signerInfo =
(org.bouncycastle.cms.SignerInformation) signedData.getSignerInfos().getSigners().iterator().next();
String digestOid = signerInfo.getDigestAlgOID();
String encryptionOid = signerInfo.getEncryptionAlgOID();
LOGGER.debug("İmza algoritması OID'leri: digest={}, encryption={}", digestOid, encryptionOid);
response.setSignatureAlgorithmOid(encryptionOid);
// Önce encryptionOid'yi direkt SignatureAlgorithm olarak dene
try {
SignatureAlgorithm sigAlg = SignatureAlgorithm.forOID(encryptionOid);
String algName = sigAlg.name().replace("_", " with ");
response.setSignatureAlgorithm(algName);
LOGGER.debug("İmza algoritması bulundu: {}", algName);
} catch (Exception ex) {
// SignatureAlgorithm kombinasyonunu oluştur
try {
DigestAlgorithm digestAlg = DigestAlgorithm.forOID(digestOid);
EncryptionAlgorithm encAlg = EncryptionAlgorithm.forOID(encryptionOid);
SignatureAlgorithm sigAlg = SignatureAlgorithm.getAlgorithm(encAlg, digestAlg);
String algName = sigAlg.name().replace("_", " with ");
response.setSignatureAlgorithm(algName);
LOGGER.debug("İmza algoritması kombinasyonu oluşturuldu: {}", algName);
} catch (Exception ex2) {
// Fallback: Manuel isim oluştur
try {
DigestAlgorithm digestAlg = DigestAlgorithm.forOID(digestOid);
EncryptionAlgorithm encAlg = EncryptionAlgorithm.forOID(encryptionOid);
String algName = digestAlg.getName() + " with " + encAlg.getName();
response.setSignatureAlgorithm(algName);
} catch (Exception ex3) {
LOGGER.debug("İmza algoritması DSS'de bulunamadı, OID kullanılıyor: {}", encryptionOid);
response.setSignatureAlgorithm(encryptionOid);
}
}
}
}
} catch (Exception e) {
LOGGER.warn("İmza algoritması bilgisi alınamadı: {}", e.getMessage());
}
if (bcToken.getTimeStampInfo().getNonce() != null) {
response.setNonce(bcToken.getTimeStampInfo().getNonce().toString());
}
// TSA sertifikasını al ve doğrula
X509CertificateHolder signerCert = getSignerCertificate(bcToken);
if (signerCert != null) {
response.setTsaName(signerCert.getSubject().toString());
response.setTsaCertificate(Base64.getEncoder().encodeToString(signerCert.getEncoded()));
// Sertifika geçerlilik tarihlerini kontrol et
Date now = new Date();
response.setCertificateNotBefore(ISO_DATE_FORMAT.format(signerCert.getNotBefore()));
response.setCertificateNotAfter(ISO_DATE_FORMAT.format(signerCert.getNotAfter()));
boolean certValid = now.after(signerCert.getNotBefore()) && now.before(signerCert.getNotAfter());
response.setCertificateValid(certValid);
if (!certValid) {
errors.add("TSA sertifikası geçerlilik tarihleri dışında");
}
LOGGER.info("Zaman damgası sertifikası bulundu ve imza geçerli");
} else {
errors.add("TSA sertifikası bulunamadı");
}
// Orijinal veri sağlanmışsa hash doğrulaması yap
if (originalData != null) {
try {
byte[] messageImprint = bcToken.getTimeStampInfo().getMessageImprintDigest();
// Hash'i hesapla
String messageImprintOid = bcToken.getTimeStampInfo().getMessageImprintAlgOID().getId();
DigestAlgorithm digestAlgorithm = getDigestAlgorithmByOid(messageImprintOid);
byte[] computedHash = computeDigest(originalData, digestAlgorithm);
boolean hashMatch = Arrays.equals(messageImprint, computedHash);
response.setHashVerified(hashMatch);
if (!hashMatch) {
errors.add("Belge hash'i eşleşmiyor - belge değiştirilmiş olabilir");
} else {
LOGGER.info("Belge hash'i doğrulandı");
}
} catch (Exception e) {
errors.add("Hash doğrulaması yapılamadı: " + e.getMessage());
LOGGER.error("Hash doğrulaması başarısız", e);
}
}
// Genel geçerlilik durumu
boolean valid = errors.isEmpty();
response.setValid(valid);
response.setErrors(errors);
if (valid) {
response.setMessage("Zaman damgası geçerli ve doğrulandı");
LOGGER.info("Zaman damgası doğrulaması başarılı. Seri no: {}", response.getSerialNumber());
} else {
response.setMessage("Zaman damgası doğrulaması başarısız");
LOGGER.warn("Zaman damgası doğrulaması başarısız. Hatalar: {}", errors);
}
return response;
} catch (Exception e) {
LOGGER.error("Zaman damgası doğrulama hatası", e);
errors.add("Genel doğrulama hatası: " + e.getMessage());
response.setValid(false);
response.setErrors(errors);
response.setMessage("Zaman damgası doğrulanamadı");
return response;
}
}
/**
* BouncyCastle timestamp token'ı parse eder.
* Hem TimeStampResponse formatını hem de direkt TimeStampToken formatını destekler.
*/
private org.bouncycastle.tsp.TimeStampToken parseBCTimestampToken(byte[] timestampBytes) {
try {
LOGGER.debug("Timestamp token parse ediliyor, boyut: {} bytes", timestampBytes.length);
// Direkt TimeStampToken formatı dene (CMSSignedData)
try {
CMSSignedData signedData = new CMSSignedData(timestampBytes);
org.bouncycastle.tsp.TimeStampToken token = new org.bouncycastle.tsp.TimeStampToken(signedData);
LOGGER.debug("Token CMSSignedData formatında parse edildi");
return token;
} catch (Exception e) {
LOGGER.debug("CMSSignedData formatı değil: {}", e.getMessage());
}
// TimeStampResponse formatı dene
try {
TimeStampResponse tsResponse = new TimeStampResponse(timestampBytes);
org.bouncycastle.tsp.TimeStampToken token = tsResponse.getTimeStampToken();
if (token != null) {
LOGGER.debug("Token TimeStampResponse formatında parse edildi");
return token;
}
} catch (Exception e) {
LOGGER.debug("TimeStampResponse formatı değil: {}", e.getMessage());
}
LOGGER.warn("Timestamp token formatı tanınamadı");
return null;
} catch (Exception e) {
LOGGER.error("Timestamp token parse hatası", e);
return null;
}
}
/**
* Timestamp token'dan imzalayan sertifikayı alır.
*/
@SuppressWarnings("unchecked")
private X509CertificateHolder getSignerCertificate(org.bouncycastle.tsp.TimeStampToken tsToken) {
try {
@SuppressWarnings("rawtypes")
Store certStore = tsToken.getCertificates();
Collection<X509CertificateHolder> certCollection = certStore.getMatches(null);
if (!certCollection.isEmpty()) {
return certCollection.iterator().next();
}
} catch (Exception e) {
LOGGER.warn("TSA sertifikası alınamadı", e);
}
return null;
}
/**
* Verinin hash'ini hesaplar.
*/
private byte[] computeDigest(byte[] data, DigestAlgorithm algorithm) {
try {
MessageDigest digest = MessageDigest.getInstance(algorithm.getJavaName());
return digest.digest(data);
} catch (Exception e) {
throw new TimestampException("Hash hesaplanamadı: " + e.getMessage(), e);
}
}
/**
* Hash algoritmasını string'den DigestAlgorithm'a çevirir.
*/
private DigestAlgorithm getDigestAlgorithm(String algorithm) {
if (!StringUtils.hasText(algorithm)) {
return DigestAlgorithm.SHA256;
}
try {
return DigestAlgorithm.valueOf(algorithm.toUpperCase().replace("-", ""));
} catch (IllegalArgumentException e) {
throw new TimestampException("Geçersiz hash algoritması: " + algorithm);
}
}
/**
* OID'den DigestAlgorithm'a çevirir.
*/
private DigestAlgorithm getDigestAlgorithmByOid(String oid) {
for (DigestAlgorithm alg : DigestAlgorithm.values()) {
if (alg.getOid().equals(oid)) {
return alg;
}
}
return DigestAlgorithm.SHA256; // default
}
}