External signing is a GroupDocs.Signature capability for Java that hands the document digest to
a callback you provide instead of signing with a private key the application loads. Combined
with the HashiCorp Vault Transit engine it produces a signed PDF whose RSA private key was
created inside Vault, is not exportable, and is only ever asked to sign a 32-byte hash.
Five methods make up the pipeline. Each is shown with the code from the sample and the
considerations that decide whether it behaves in production. Everything here was run with
GroupDocs.Signature for Java 26.10, BouncyCastle 1.86 and Vault 2.2.1 in dev mode, on JDK 25
under Windows and JDK 17 under Linux.
What This Use Case Covers
Reading a Transit key and creating it on first use, building an X.509 certificate for a key
that cannot be exported, the signing callback itself, the DigitalSignOptions setup, and
verification. It is written for Java developers who have to move a signing key off the
application host.
Prerequisites:
JDK 8 or later, Maven 3, and Docker for the dev-mode Vault container
On Linux, the font Arial, because the visible signature is drawn with it
GroupDocs.Signature for Java 26.10; without a licence, evaluation mode accepts up to 2 pages
Why the JDK’s Own Signing Path Is Not Enough
Java can sign bytes with a PrivateKey from a KeyStore, and that is the model most PDF
tooling follows: give it a PFX, give it a password, get a signed file. The key then lives on the
application host, in a file, an image layer, a backup, or the memory of the signing process.
Permissions and encrypted volumes narrow the exposure; they do not remove the copy.
A key manager removes the copy, but only for a library that can delegate. The JDK has no
mechanism for building a certificate whose signature comes from an external service either,
which is the second half of the problem and the part developers usually discover later.
Repository Structure
SignPdfWithVault.java entry point: key, certificate, sign, verify
VaultSigner.java the callback GroupDocs.Signature invokes
CertificateHelper.java BouncyCastle certificate, signed by Vault
VaultTransitClient.java Vault Transit HTTP calls
vault/sample-signer.hcl least-privilege policy example
docker-compose.yml Vault in dev mode, local only
Method 1: Read or Create the Transit Key
How It Works
GET /v1/transit/keys/{name} returns every key version with its public key in PEM form, plus
latest_version. The private half is never part of the answer. A 404 means the key or the
Transit mount does not exist, which the sample treats as first-run setup rather than an error.
Read the version once. A certificate holds one public key, so the certificate and every
signature have to come from the same version. In production an administrator enables Transit and
creates the key, and the application’s token has no right to do either.
Method 2: Build a Certificate for a Non-Exportable Key
How It Works
BouncyCastle’s X509v3CertificateBuilder assembles the certificate and delegates the signature
to a ContentSigner. Implementing that interface against Vault yields a certificate carrying
the Vault public key and signed by the matching private key, which never leaves Vault.
The certificate starts a day early on purpose. A PDF reader compares the signing time with the
start date, and on a machine two hours east of UTC a certificate minted seconds before signing
was reported by Adobe Acrobat as expired or not yet valid. ContentSigner declares no checked
exceptions, so a Vault error is wrapped in UncheckedIOException and unwrapped by the caller.
For production, have a CA issue a certificate for the same key through
transit/keys/{name}/csr.
Method 3: The External Signing Callback
How It Works
GroupDocs.Signature calls the callback once per signature with the digest of the signed byte
range. The callback forwards it to transit/sign and returns the raw bytes.
hashAlgorithm arrives as an int constant from HashAlgorithm and is the algorithm the
library really used, so it maps one to one: Sha256 to sha2-256, Sha384 to sha2-384,
Sha512 to sha2-512, Sha1 to sha1. The Vault request must set prehashed = true, because
the input is already a hash, and signature_algorithm must be pkcs1v15, because Vault
defaults to PSS for RSA keys while the PDF signature is sha256WithRSAEncryption.
Method 4: Sign the PDF
How It Works
DigitalSignOptions carries the public-only certificate and the callback. The signed document
goes to a stream, and the file is written only after signing succeeds.
The callback goes in as a method reference because the ICustomSignHash method carries an
obfuscated name in the 26.10 jar and cannot be implemented by name. Version 26.10 also pads the
signed output with zero bytes past the end of the PDF, outside the signed range, so the sample
trims them. The signing time is a Java Date and therefore has no time zone, unlike the
offset-aware value the Python port writes.
Method 5: Verify Before Anything Is Released
How It Works
DigitalVerifyOptions built from the certificate ties the result to the expected signer, and
isValid() answers whether the signature covers the file as it stands.
In 26.10 the certificate must be a file path; the stream form throws a NullPointerException.
The sample therefore writes Result/vault-signer.cer and verifies against it. A self-signed
certificate satisfies the cryptographic check, while Acrobat additionally wants a trusted issuer
and reports the signer’s identity as unknown without one.
Which failure should I guard against first?
The silent one. A wrong algorithm name fails loudly, with Vault answering
500 crypto/rsa: input must be hashed message. A missing prehashed flag produces no error at
all: Vault hashes the digest again, the signature covers the wrong value, and the PDF looks
finished. Method 5 in the same run is what turns that into an immediate failure.
Common Questions
Does the document leave the JVM?
No. Only the digest is sent, and Transit does not store what it receives.
Can the same code use an HSM or a cloud KMS?
Yes. The callback has no idea what is behind it; replace the Vault client with the other
service’s signing call.
What happens after a key rotation?
Vault signs with the newest version unless told otherwise, so a certificate for version 1 and a
signature from version 2 give a PDF that reports no error and does not verify. Pinning the
version prevents it.
Why does signing fail on a Linux container?
The visible signature box is drawn with Arial. Install the font, or turn the visible box off.
Summary and When to Use Which Method
Methods 1 and 2 are setup and run once per key; methods 3 to 5 are the signing path. Use the
whole set when the key must stay in a key manager. If the key may live in a PFX, the standard
digital-signature options are simpler and these five methods are unnecessary.
I kept the version pinning from the .NET build when porting this, and that choice is what makes
the certificate and the signature agree after somebody rotates the key in Vault.