Skip to content
DemoSampleAPI ReferenceFAQ

PDF Generation Template Editor

Open-source visual PDF generation engine with customizable templates and developer-friendly APIs.

View on GitHub

Two-Phase External Signing ​

For SDK requirements and common imports, see Digital Signatures.

Two-phase signing separates PDF preparation from the external signature operation. It supports delayed responses, another JVM, or another machine without keeping the original document or a native pointer alive.

  1. Prepare: create a new signature field, reserve /Contents, set /ByteRange, and return the digest and portable Session.
  2. External signing: obtain a signature for this preparation from the external service.
  3. Fill: write the returned CMS container or raw signature into the unchanged prepared PDF.
  4. Validate: inspect the complete verification report.
Hash modeWhat the external service signsFill method
DOCUMENT_DIGESTDigest of the PDF bytes covered by /ByteRangeComplete CMS or raw signature value
SIGNED_ATTRIBUTESDigest of frozen CMS signed attributesRaw signature value

Supported digest algorithms are SHA-256, SHA-384, and SHA-512. The raw RSA contract is PKCS#1 v1.5; the current interface does not represent RSA-PSS parameters.

Prepare a Signature

Signed-attributes mode requires the signer certificate during preparation because the attributes bind that certificate. The default field is invisible. The default reservation is 65536 binary bytes for the complete CMS container.

java
Files.createDirectories(Paths.get("output"));
CPDFExternalSignPrepareOptions options = new CPDFExternalSignPrepareOptions()
        .setFieldName("ExternalSignature")
        .setHashMode(CPDFExternalSignPrepareOptions.HashMode.SIGNED_ATTRIBUTES)
        .setDigestAlgorithm(CPDFSignatureDigest.SHA256)
        .setEstimatedContentsSize(65536)
        .setCertificate(
                Files.readAllBytes(Paths.get("certs/signer.der")),
                new byte[][] {Files.readAllBytes(Paths.get("certs/issuer.der"))});

CPDFExternalSignPrepareResult prepared = CPDFExternalSignSession.prepare(
        "input.pdf", "output/prepared.pdf", options);
if (!prepared.isSuccess()) {
    throw new IllegalStateException(prepared.getErrorCode() + ": " + prepared.getErrorMessage());
}
CPDFExternalSignSession.saveSession("output/signing-session.txt", prepared.getSession());
Files.write(Paths.get("output/hash-to-sign.bin"), prepared.getHashToSign());
System.out.println("Digest algorithm: " + prepared.getDigestAlgorithm());
System.out.println("Hash to sign: "
        + Base64.getEncoder().encodeToString(prepared.getHashToSign()));

The hash is already digested. Do not hash it again as an ordinary message and do not sign its Base64 text. Keep the prepared PDF, complete Session, hash, and digest algorithm associated with the same transaction. Repeating Prepare may change the bytes, signing time, and hash.

saveSession() creates a new UTF-8 file and does not overwrite an existing one. The Session is the Core format, not a Java object serialization. Preserve it without trimming or rewriting; the application is responsible for saving the transaction successfully.

Fill an External Raw Signature

This example can run in another process after the service has returned external-signature.bin for the preceding preparation:

java
String session = CPDFExternalSignSession.loadSession("output/signing-session.txt");
byte[] signatureValue = Files.readAllBytes(Paths.get("external-signature.bin"));
CPDFExternalSignFillOptions options = new CPDFExternalSignFillOptions()
        .setValidationOptions(new CPDFSignatureValidationOptions()
                .setTrustedCertPath("certs/trusted-roots.pem"));
CPDFExternalSignFillResult result = CPDFExternalSignSession.fillWithSignatureValue(
        session,
        "output/prepared.pdf",
        "output/external-signed.pdf",
        signatureValue,
        options);
System.out.println("Fill success: " + result.isSuccess());
System.out.println(result.getErrorCode() + ": " + result.getErrorMessage());
System.out.println(result.getValidationResult());

For SIGNED_ATTRIBUTES, Core uses the certificate and attributes frozen in the Session. For DOCUMENT_DIGEST, supply the certificate at Prepare or through CPDFExternalSignFillOptions.setCertificate() at Fill.

The prepared file can move after preparation, provided its exact bytes and Session are preserved. It is kept after a failed Fill so that the application can retry with the correct payload.

Fill a Complete CMS Container

Prepare a separate DOCUMENT_DIGEST transaction when the external service returns a complete detached CMS/PKCS#7 container. A container with signed attributes must carry the correct messageDigest for the prepared PDF's /ByteRange.

java
Files.createDirectories(Paths.get("output"));
CPDFExternalSignPrepareResult prepared = CPDFExternalSignSession.prepare(
        "input.pdf",
        "output/container-prepared.pdf",
        new CPDFExternalSignPrepareOptions()
                .setHashMode(CPDFExternalSignPrepareOptions.HashMode.DOCUMENT_DIGEST));
if (!prepared.isSuccess()) {
    throw new IllegalStateException(prepared.getErrorCode());
}
CPDFExternalSignSession.saveSession("output/container-session.txt", prepared.getSession());

// Obtain this CMS from the service for the preparation above.
byte[] containerDer = Files.readAllBytes(Paths.get("external-container.der"));
CPDFExternalSignFillResult result = CPDFExternalSignSession.fillWithContainer(
        CPDFExternalSignSession.loadSession("output/container-session.txt"),
        "output/container-prepared.pdf",
        "output/container-signed.pdf",
        containerDer,
        new CPDFExternalSignFillOptions()
                .setVerifyMessageDigest(true)
                .setValidationOptions(new CPDFSignatureValidationOptions()
                        .setTrustedCertPath("certs/trusted-roots.pem")));
System.out.println(result.getValidationResult());

The payload must be one complete detached SignedData DER value with signer information. Attached CMS, malformed containers, and extra trailing data are rejected. The reservation must fit the entire encoded container, including certificates and timestamps. A larger reservation requires a new Prepare operation and a new external signature.

Memory-Based Signing

Prepare and both Fill methods also have byte[] overloads. Retrieve prepared bytes with getPreparedPdf() and completed bytes with getOutputPdf(). File preparation and memory Fill, or memory preparation and file Fill, can use the same Core Session.

java
byte[] inputPdf = Files.readAllBytes(Paths.get("input.pdf"));
CPDFExternalSignPrepareResult prepared = CPDFExternalSignSession.prepare(
        inputPdf,
        new CPDFExternalSignPrepareOptions()
                .setHashMode(CPDFExternalSignPrepareOptions.HashMode.SIGNED_ATTRIBUTES)
                .setCertificate(
                        Files.readAllBytes(Paths.get("certs/signer.der")),
                        new byte[][] {Files.readAllBytes(Paths.get("certs/issuer.der"))}));
if (!prepared.isSuccess()) {
    throw new IllegalStateException(prepared.getErrorCode());
}

// Obtain the service response for prepared.getHashToSign().
byte[] signatureValue = Files.readAllBytes(Paths.get("external-memory-signature.bin"));
CPDFExternalSignFillResult result = CPDFExternalSignSession.fillWithSignatureValue(
        prepared.getSession(), prepared.getPreparedPdf(), signatureValue,
        new CPDFExternalSignFillOptions()
                .setValidationOptions(new CPDFSignatureValidationOptions()
                        .setTrustedCertPath("certs/trusted-roots.pem")));
if (result.isSuccess()) {
    Files.createDirectories(Paths.get("output"));
    Files.write(Paths.get("output/memory-signed.pdf"), result.getOutputPdf());
}
System.out.println(result.getValidationResult());

Memory Prepare and Fill do not write PDF files. Optional validation uses a seekable source containing the original bytes: an anonymous memory file on Linux when available, otherwise a private temporary byte-for-byte copy removed after validation. The PDF is not reserialized.

Fill success only means that a PDF was produced and, for file APIs, published. It does not prove that a returned signature is correct or trusted. Verification is enabled by default; inspect getValidationResult(). Disabled verification reports NOT_CHECKED.

Two-phase signatures use adbe.pkcs7.detached. They do not automatically collect LTV material or inject an embedded timestamp. An external service may supply a timestamped complete container, or the application may append an independent document timestamp after Fill.

See SignatureTwoPhaseTest for fixed-configuration Prepare/Fill operation and recovery.