PAdESSignatureService.java
package io.mersel.dss.signer.api.services.signature.pades;
import com.itextpdf.text.pdf.*;
import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import io.mersel.dss.signer.api.exceptions.SignatureException;
import io.mersel.dss.signer.api.models.SignResponse;
import io.mersel.dss.signer.api.models.SigningMaterial;
import io.mersel.dss.signer.api.services.crypto.DigestAlgorithmResolverService;
import io.mersel.dss.signer.api.services.crypto.SigningMaterialContentSigner;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.ess.ESSCertIDv2;
import org.bouncycastle.asn1.ess.SigningCertificateV2;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.IssuerSerial;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.Calendar;
import java.util.HashMap;
import java.util.concurrent.Semaphore;
/**
* PAdES (PDF İleri Seviye Elektronik İmza) imzaları oluşturan servis.
* iText ve BouncyCastle kullanarak CAdES tabanlı PDF imzalama yapar.
*
* <p>Özellikler:
* <ul>
* <li>Gömülü CAdES imzası</li>
* <li>Dosya eki desteği</li>
* <li>Çoklu imza için ekleme modu</li>
* <li>SigningCertificateV2 özniteliği</li>
* </ul>
*/
@Service
public class PAdESSignatureService {
private static final Logger LOGGER = LoggerFactory.getLogger(PAdESSignatureService.class);
private static final int SIGNATURE_SIZE_ESTIMATE = 8192;
private final Semaphore semaphore;
private final DigestAlgorithmResolverService digestAlgorithmResolver;
public PAdESSignatureService(Semaphore signatureSemaphore,
DigestAlgorithmResolverService digestAlgorithmResolver) {
this.semaphore = signatureSemaphore;
this.digestAlgorithmResolver = digestAlgorithmResolver;
}
/**
* PDF belgesini PAdES imzası ile imzalar.
*
* @param pdfInputStream PDF belgesi içeren input stream
* @param attachment İsteğe bağlı dosya eki içeriği
* @param attachmentFileName İsteğe bağlı ek dosya adı
* @param appendMode İmzanın eklenmesi (true) veya yeni revizyon (false)
* @param material İmzalama sertifikası ve private key içeren materyal
* @return İmzalanmış PDF içeren yanıt
*/
public SignResponse signPdf(InputStream pdfInputStream,
byte[] attachment,
String attachmentFileName,
boolean appendMode,
SigningMaterial material) {
try {
PdfReader reader = new PdfReader(pdfInputStream);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfStamper stamper = PdfStamper.createSignature(
reader, outputStream, '\0', null, appendMode);
// Dosya eki varsa ekle
if (attachment != null && attachment.length > 0 && attachmentFileName != null) {
stamper.addFileAttachment(null, attachment, null, attachmentFileName);
}
// İmza görünümünü yapılandır
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setLocation("Turkey");
appearance.setSignDate(Calendar.getInstance());
// İmza sözlüğünü oluştur
PdfSignature pdfSignature = new PdfSignature(
PdfName.ADOBE_PPKLITE, PdfName.ETSI_CADES_DETACHED);
pdfSignature.setReason(appearance.getReason());
pdfSignature.setLocation(appearance.getLocation());
pdfSignature.setContact(appearance.getContact());
pdfSignature.setDate(new PdfDate(appearance.getSignDate()));
appearance.setCryptoDictionary(pdfSignature);
// İmza için yer ayır
HashMap<PdfName, Integer> exclusionSizes = new HashMap<>();
exclusionSizes.put(PdfName.CONTENTS, SIGNATURE_SIZE_ESTIMATE * 2 + 2);
appearance.preClose(exclusionSizes);
// CMS imzasını oluştur
byte[] signatureBytes = createCMSSignature(
appearance, material);
// İmzayı göm
PdfDictionary dictionary = new PdfDictionary();
dictionary.put(PdfName.CONTENTS,
new PdfString(signatureBytes).setHexWriting(true));
appearance.close(dictionary);
LOGGER.info("PAdES imzası başarıyla oluşturuldu");
return new SignResponse(outputStream.toByteArray(), null);
} catch (Exception e) {
LOGGER.error("PAdES imzası oluşturulurken hata", e);
throw new SignatureException("PAdES imzası oluşturulamadı", e);
}
}
/**
* PDF içeriği için CMS imzası oluşturur.
* SigningCertificateV2 özniteliği ile sertifikaya uygun hash algoritması kullanır.
*
* <p>Digest algoritması seçimi {@link DigestAlgorithmResolverService}'e
* delege edilir — CAdES/XAdES ile <b>aynı</b> mantık (sertifika sigAlg
* adı + EC anahtar boyu, NIST SP 800-57). Bu sayede aynı sertifikayla
* farklı imza formatlarında tutarlı digest kullanılır.</p>
*/
private byte[] createCMSSignature(PdfSignatureAppearance appearance,
SigningMaterial material) throws Exception {
DigestAlgorithm digest = digestAlgorithmResolver.resolveDigestAlgorithm(
material.getSigningCertificate());
String digestAlgName = digest.getJavaName();
org.bouncycastle.asn1.ASN1ObjectIdentifier digestOid =
new org.bouncycastle.asn1.ASN1ObjectIdentifier(digest.getOid());
// SigningCertificateV2 için sertifika hash'i hesapla
MessageDigest messageDigest = MessageDigest.getInstance(digestAlgName);
byte[] certificateHash = messageDigest.digest(
material.getSigningCertificate().getEncoded());
// Issuer serial oluştur
GeneralName generalName = new GeneralName(
X500Name.getInstance(material.getSigningCertificate()
.getIssuerX500Principal().getEncoded()));
GeneralNames generalNames = new GeneralNames(generalName);
IssuerSerial issuerSerial = new IssuerSerial(
generalNames, material.getSigningCertificate().getSerialNumber());
// Create SigningCertificateV2 attribute
ESSCertIDv2 essCert = new ESSCertIDv2(
new AlgorithmIdentifier(digestOid),
certificateHash, issuerSerial);
SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(
new ESSCertIDv2[]{essCert});
Attribute signingCertAttr = new Attribute(
PKCSObjectIdentifiers.id_aa_signingCertificateV2,
new DERSet(signingCertificateV2));
// Build signed attributes
ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
signedAttributes.add(signingCertAttr);
AttributeTable attributeTable = new AttributeTable(signedAttributes);
// Create signer
JcaSignerInfoGeneratorBuilder signerInfoGeneratorBuilder =
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build())
.setSignedAttributeGenerator(
new DefaultSignedAttributeTableGenerator(attributeTable));
// BC CMS imzasını SigningMaterial'ın tek sign kontratı üzerinden ata.
// PFX ve HSM ayrımı document-format katmanına sızmaz.
ContentSigner contentSigner = buildContentSigner(material, digest);
// Generate CMS signed data
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
generator.addSignerInfoGenerator(
signerInfoGeneratorBuilder.build(contentSigner,
material.getSigningCertificate()));
generator.addCertificates(new JcaCertStore(material.getCertificateChain()));
// Sign PDF content
InputStream rangeStream = appearance.getRangeStream();
byte[] rangeBytes = IOUtils.toByteArray(rangeStream);
semaphore.acquire();
try {
CMSSignedData signedData = generator.generate(
new CMSProcessableByteArray(rangeBytes), false);
byte[] encodedSignature = signedData.getEncoded();
if (encodedSignature.length > SIGNATURE_SIZE_ESTIMATE) {
throw new SignatureException(
"Signature size exceeds reserved space: " +
encodedSignature.length + " > " + SIGNATURE_SIZE_ESTIMATE);
}
// Pad signature to reserved size
byte[] paddedSignature = new byte[SIGNATURE_SIZE_ESTIMATE];
System.arraycopy(encodedSignature, 0, paddedSignature, 0,
encodedSignature.length);
return paddedSignature;
} finally {
semaphore.release();
}
}
/**
* Material'in arka ucundan bağımsız tek {@link ContentSigner} üretir.
*/
private ContentSigner buildContentSigner(SigningMaterial material, DigestAlgorithm digest) throws Exception {
return new SigningMaterialContentSigner(material, digest);
}
}