Search code examples
javapdfdigital-signaturebouncycastlepades

Base64 digest + PFX(PKCS12) -> ETSI.CAdES.detached signature -> PAdES LTV


I have an API that creates Base64 digest of a PDF Document. Now I want to create another API that takes this digest and PFX and creates an ETSI.CAdES.detached signature and takes LTV informations(Certs chain,OCSP response,CRL) that I want to embed in my PDF to obtain a PAdES-LTV signature using 3rd API(My 3rd API will take CAdES signature and LTV informations obtained from this API and will embed them in my PDF).I dont know how to create this ETSI.CAdES.detached signature using that digest and a PFX with Java and Bouncy Castle.I try to follow this github tutorial.


Solution

  • As you have declared, you have your own code for preparing a PDF for signing and for injecting the signature container into it. Thus, your question essentially burns down to

    How to create a CAdES signature container with BouncyCastle that can be used to create a PAdES BASELINE B or T PDF signature?

    Implementation in the iText 7 Signing Framework

    As I do not have your existing code, I had to use a different framework for my tests. I used the iText 7 signing framework for that.

    BouncyCastle does contain a CMSSignedDataGenerator to generate CMS signature containers.

    The default implementation of the SignerInfo generation therein unfortunately is not CAdES/PAdES compatible as it does not create signed ESSCertID[v2] attributes. Fortunately, though, the implementation is designed to allow plugging in custom attributes sets.

    Thus, you can create the CAdES containers required for PAdES BASELINE signatures with a customized CMSSignedDataGenerator.

    So when you have prepared the PDF for signing, you can proceed like this:

    InputStream data = [InputStream containing the PDF byte ranges to sign];
    ContentSigner contentSigner = [BouncyCastle ContentSigner for your private key];
    X509CertificateHolder x509CertificateHolder = [BouncyCastle X509CertificateHolder for your X.509 signer certificate];
    
    DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
    
    
    CMSTypedData msg = new CMSTypedDataInputStream(data);
    
    CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
    
    gen.addSignerInfoGenerator(
            new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
                    .setSignedAttributeGenerator(new PadesSignedAttributeGenerator())
                    .setUnsignedAttributeGenerator(new PadesUnsignedAttributeGenerator())
                    .build(contentSigner, x509CertificateHolder));
    
    gen.addCertificates(new JcaCertStore(Collections.singleton(x509CertificateHolder)));
    
    CMSSignedData sigData = gen.generate(msg, false);
    byte[] cmsBytes = sigData.getEncoded();
    

    (PadesSignatureContainerBc method sign)

    The byte[] cmsBytes contains the bytes to inject into the prepared PDF signature placeholder.

    The following helper classes are needed:

    First of all a wrapper for the InputStream containing the PDF ranges to sign to process by BouncyCastle.

    class CMSTypedDataInputStream implements CMSTypedData {
        InputStream in;
    
        public CMSTypedDataInputStream(InputStream is) {
            in = is;
        }
    
        @Override
        public ASN1ObjectIdentifier getContentType() {
            return PKCSObjectIdentifiers.data;
        }
    
        @Override
        public Object getContent() {
            return in;
        }
    
        @Override
        public void write(OutputStream out) throws IOException,
                CMSException {
            byte[] buffer = new byte[8 * 1024];
            int read;
            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
            in.close();
        }
    }
    

    (PadesSignatureContainerBc helper class CMSTypedDataInputStream)

    Then a customized signed attributes generator for PAdES:

    class PadesSignedAttributeGenerator implements CMSAttributeTableGenerator {
        @Override
        public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
            String currentAttribute = null;
            try {
                ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
                currentAttribute = "SigningCertificateAttribute";
                AlgorithmIdentifier digAlgId = (AlgorithmIdentifier) params.get(CMSAttributeTableGenerator.DIGEST_ALGORITHM_IDENTIFIER);
                signedAttributes.add(createSigningCertificateAttribute(digAlgId));
                currentAttribute = "ContentTypeAttribute";
                ASN1ObjectIdentifier contentType = ASN1ObjectIdentifier.getInstance(params.get(CMSAttributeTableGenerator.CONTENT_TYPE));
                signedAttributes.add(new Attribute(CMSAttributes.contentType, new DERSet(contentType)));
                currentAttribute = "MessageDigestAttribute";
                byte[] messageDigest = (byte[])params.get(CMSAttributeTableGenerator.DIGEST);
                signedAttributes.add(new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest))));
    
                return new AttributeTable(signedAttributes);
            } catch (Exception e) {
                throw new CMSAttributeTableGenerationException(currentAttribute, e);
            }
        }
    
        Attribute createSigningCertificateAttribute(AlgorithmIdentifier digAlg) throws IOException, OperatorCreationException {
            final IssuerSerial issuerSerial = getIssuerSerial();
            DigestCalculator digestCalculator = digestCalculatorProvider.get(digAlg);
            digestCalculator.getOutputStream().write(x509CertificateHolder.getEncoded());
            final byte[] certHash = digestCalculator.getDigest();
    
            if (OIWObjectIdentifiers.idSHA1.equals(digAlg.getAlgorithm())) {
                final ESSCertID essCertID = new ESSCertID(certHash, issuerSerial);
                SigningCertificate signingCertificate = new SigningCertificate(essCertID);
                return new Attribute(id_aa_signingCertificate, new DERSet(signingCertificate));
            } else {
                ESSCertIDv2 essCertIdv2;
                if (NISTObjectIdentifiers.id_sha256.equals(digAlg.getAlgorithm())) {
                    // SHA-256 is default
                    essCertIdv2 = new ESSCertIDv2(null, certHash, issuerSerial);
                } else {
                    essCertIdv2 = new ESSCertIDv2(digAlg, certHash, issuerSerial);
                }
                SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(essCertIdv2);
                return new Attribute(id_aa_signingCertificateV2, new DERSet(signingCertificateV2));
            }
        }
    
        IssuerSerial getIssuerSerial() {
            final X500Name issuerX500Name = x509CertificateHolder.getIssuer();
            final GeneralName generalName = new GeneralName(issuerX500Name);
            final GeneralNames generalNames = new GeneralNames(generalName);
            final BigInteger serialNumber = x509CertificateHolder.getSerialNumber();
            return new IssuerSerial(generalNames, serialNumber);
        }
    }
    

    (PadesSignatureContainerBc helper class PadesSignedAttributeGenerator )

    And finally a customized unsigned attributes generator for a signature timestamp:

    class PadesUnsignedAttributeGenerator implements CMSAttributeTableGenerator {
        @Override
        public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
            if (tsaClient == null)
                return null;
            try {
                ASN1EncodableVector unsignedAttributes = new ASN1EncodableVector();
                byte[] signature = (byte[])params.get(CMSAttributeTableGenerator.SIGNATURE);
                byte[] timestamp = tsaClient.getTimeStampToken(tsaClient.getMessageDigest().digest(signature));
                unsignedAttributes.add(new Attribute(id_aa_signatureTimeStampToken, new DERSet(ASN1Primitive.fromByteArray(timestamp))));
                return new AttributeTable(unsignedAttributes);
            } catch (Exception e) {
                throw new CMSAttributeTableGenerationException("", e);
            }
        }
    }
    

    (PadesSignatureContainerBc helper class PadesUnsignedAttributeGenerator)

    Here I assume a ITSAClient tsaClient, an iText 7 time stamp request client. You can of course use an arbitrary RFC 3161 time stamp request client of your choice.

    If you have read your private key into a JCA/JCE PrivateKey pk, you can simply create the needed ContentSigner contentSigner using the BouncyCastle JcaContentSignerBuilder, e.g. like this:

    ContentSigner contentSigner = new JcaContentSignerBuilder("SHA512withRSA").build(pk);
    

    (compare the test testSignPadesBaselineT in SignPadesBc)

    Implementation in the PDFBox 3 Signing Framework

    You meanwhile indicated in comments that you're looking into using PDFBox to sign. Fortunately the code presented above can nearly without a change be used with PDFBox.

    To use the code above with PDFBox, one merely has to wrap it into a PDFBox SignatureInterface frame:

    public class PadesSignatureContainerBc implements SignatureInterface {
    
        public PadesSignatureContainerBc(X509CertificateHolder x509CertificateHolder, ContentSigner contentSigner, TSAClient tsaClient) throws OperatorCreationException {
            this.contentSigner = contentSigner;
            this.tsaClient = tsaClient;
            this.x509CertificateHolder = x509CertificateHolder;
    
            digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
        }
    
        @Override
        public byte[] sign(InputStream content) throws IOException {
            try {
                CMSTypedData msg = new CMSTypedDataInputStream(content);
    
                CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
    
                gen.addSignerInfoGenerator(
                        new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
                                .setSignedAttributeGenerator(new PadesSignedAttributeGenerator())
                                .setUnsignedAttributeGenerator(new PadesUnsignedAttributeGenerator())
                                .build(contentSigner, x509CertificateHolder));
    
                gen.addCertificates(new JcaCertStore(Collections.singleton(x509CertificateHolder)));
    
                CMSSignedData sigData = gen.generate(msg, false);
                return sigData.getEncoded();
            } catch (OperatorCreationException | GeneralSecurityException | CMSException e) {
                throw new IOException(e);
            }
        }
    
        final ContentSigner contentSigner;
        final X509CertificateHolder x509CertificateHolder;
        final TSAClient tsaClient;
    
        final DigestCalculatorProvider digestCalculatorProvider;
    
        class CMSTypedDataInputStream implements CMSTypedData {
            InputStream in;
    
            public CMSTypedDataInputStream(InputStream is) {
                in = is;
            }
    
            @Override
            public ASN1ObjectIdentifier getContentType() {
                return PKCSObjectIdentifiers.data;
            }
    
            @Override
            public Object getContent() {
                return in;
            }
    
            @Override
            public void write(OutputStream out) throws IOException,
                    CMSException {
                byte[] buffer = new byte[8 * 1024];
                int read;
                while ((read = in.read(buffer)) != -1) {
                    out.write(buffer, 0, read);
                }
                in.close();
            }
        }
    
        class PadesSignedAttributeGenerator implements CMSAttributeTableGenerator {
            @Override
            public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
                String currentAttribute = null;
                try {
                    ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
                    currentAttribute = "SigningCertificateAttribute";
                    AlgorithmIdentifier digAlgId = (AlgorithmIdentifier) params.get(CMSAttributeTableGenerator.DIGEST_ALGORITHM_IDENTIFIER);
                    signedAttributes.add(createSigningCertificateAttribute(digAlgId));
                    currentAttribute = "ContentType";
                    ASN1ObjectIdentifier contentType = ASN1ObjectIdentifier.getInstance(params.get(CMSAttributeTableGenerator.CONTENT_TYPE));
                    signedAttributes.add(new Attribute(CMSAttributes.contentType, new DERSet(contentType)));
                    currentAttribute = "MessageDigest";
                    byte[] messageDigest = (byte[])params.get(CMSAttributeTableGenerator.DIGEST);
                    signedAttributes.add(new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(messageDigest))));
    
                    return new AttributeTable(signedAttributes);
                } catch (Exception e) {
                    throw new CMSAttributeTableGenerationException(currentAttribute, e);
                }
            }
    
            Attribute createSigningCertificateAttribute(AlgorithmIdentifier digAlg) throws IOException, OperatorCreationException {
                final IssuerSerial issuerSerial = getIssuerSerial();
                DigestCalculator digestCalculator = digestCalculatorProvider.get(digAlg);
                digestCalculator.getOutputStream().write(x509CertificateHolder.getEncoded());
                final byte[] certHash = digestCalculator.getDigest();
    
                if (OIWObjectIdentifiers.idSHA1.equals(digAlg.getAlgorithm())) {
                    final ESSCertID essCertID = new ESSCertID(certHash, issuerSerial);
                    SigningCertificate signingCertificate = new SigningCertificate(essCertID);
                    return new Attribute(id_aa_signingCertificate, new DERSet(signingCertificate));
                } else {
                    ESSCertIDv2 essCertIdv2;
                    if (NISTObjectIdentifiers.id_sha256.equals(digAlg.getAlgorithm())) {
                        // SHA-256 is default
                        essCertIdv2 = new ESSCertIDv2(null, certHash, issuerSerial);
                    } else {
                        essCertIdv2 = new ESSCertIDv2(digAlg, certHash, issuerSerial);
                    }
                    SigningCertificateV2 signingCertificateV2 = new SigningCertificateV2(essCertIdv2);
                    return new Attribute(id_aa_signingCertificateV2, new DERSet(signingCertificateV2));
                }
            }
    
            public IssuerSerial getIssuerSerial() {
                final X500Name issuerX500Name = x509CertificateHolder.getIssuer();
                final GeneralName generalName = new GeneralName(issuerX500Name);
                final GeneralNames generalNames = new GeneralNames(generalName);
                final BigInteger serialNumber = x509CertificateHolder.getSerialNumber();
                return new IssuerSerial(generalNames, serialNumber);
            }
        }
    
        class PadesUnsignedAttributeGenerator implements CMSAttributeTableGenerator {
            @Override
            public AttributeTable getAttributes(@SuppressWarnings("rawtypes") Map params) throws CMSAttributeTableGenerationException {
                if (tsaClient == null)
                    return null;
                try {
                    ASN1EncodableVector unsignedAttributes = new ASN1EncodableVector();
                    byte[] signature = (byte[])params.get(CMSAttributeTableGenerator.SIGNATURE);
                    byte[] timestamp = tsaClient.getTimeStampToken(new ByteArrayInputStream(signature)).getEncoded();
                    unsignedAttributes.add(new Attribute(id_aa_signatureTimeStampToken, new DERSet(ASN1Primitive.fromByteArray(timestamp))));
                    return new AttributeTable(unsignedAttributes);
                } catch (Exception e) {
                    throw new CMSAttributeTableGenerationException("", e);
                }
            }
        }
    }
    

    (PDFBox PadesSignatureContainerBc implementation of SignatureInterface)

    You can use it like this

    try (   PDDocument pdDocument = Loader.loadPDF(SOURCE_PDF)   )
    {
        SignatureInterface signatureInterface = new PadesSignatureContainerBc(new X509CertificateHolder(chain[0].getEncoded()),
                new JcaContentSignerBuilder("SHA512withRSA").build(pk),
                new TSAClient(new URL("http://timestamp.server/rfc3161endpoint"), null, null, MessageDigest.getInstance("SHA-256")));
    
        PDSignature signature = new PDSignature();
        signature.setFilter(COSName.getPDFName("MKLx_PAdES_SIGNER"));
        signature.setSubFilter(COSName.getPDFName("ETSI.CAdES.detached"));
        signature.setName("Example User");
        signature.setLocation("Los Angeles, CA");
        signature.setReason("Testing");
        signature.setSignDate(Calendar.getInstance());
        pdDocument.addSignature(signature);
    
        ExternalSigningSupport externalSigning = pdDocument.saveIncrementalForExternalSigning(RESULT_OUTPUT);
        // invoke external signature service
        byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent());
        // set signature bytes received from the service
        externalSigning.setSignature(cmsSignature);
    }
    

    (PDFBox SignPadesBc test testSignPadesBaselineT)