PDF signing utility written in Rust that supports both OpenPGP (GPG) and Sigstore (keyless OIDC) signatures, appending cryptographic signatures directly to PDFs, making it easy to sign and verify documents without heavyweight PDF signing stacks, making your PDFs authentic, tamper-proof, while being fully compatible with regular readers.
With pdf-sign, anyone can sign a PDF using their existing Google, Microsoft, or GitHub account – no cryptographic keys to generate, store, or manage. For power users and security-conscious workflows, it also supports GPG with full hardware key (YubiKey/smartcard) integration. Whether you're a huge company automating signatures, or just need to sign a contract, pdf-sign gets out of your way.
Many "enterprise PDF signing" solutions require a full CMS/PKCS#7 / X.509 PKI toolchain (certificate chains, policy constraints, CRL/OCSP revocation, time-stamping/TSAs) plus PDF-form machinery to produce PAdES signatures. Those stacks are powerful, but complex to configure, audit, and automate.
pdf-sign intentionally stays minimal and scriptable:
%%EOF.nix profile install github:0x77dev/pdf-sign#pdf-sign
pdf-sign --help
cargo install --git https://github.com/0x77dev/pdf-sign --locked
# GPG signing (default backend)
pdf-sign sign document.pdf --key 0xDEADBEEF
# Sigstore keyless signing
pdf-sign sign --backend sigstore document.pdf
# Verify (automatically handles both GPG and Sigstore)
pdf-sign verify document_signed.pdf
# Clone and build
git clone https://github.com/0x77dev/pdf-sign
cd pdf-sign
cargo build --release
./target/release/pdf-sign --help
# Or with Nix flake
nix build
./result/bin/pdf-sign --help
Multi-platform images (linux/amd64, linux/arm64, darwin/arm64) are published to GHCR with build provenance attestations.
docker pull ghcr.io/0x77dev/pdf-sign
# Verify attestation
gh attestation verify oci://ghcr.io/0x77dev/pdf-sign:latest --repo 0x77dev/pdf-sign
GPG signing — mount your host GPG agent socket and keyring (Linux only — macOS Docker Desktop cannot forward Unix sockets):
docker run --rm \
-v "$(gpgconf --list-dirs agent-socket)":/gnupg/S.gpg-agent \
-v ~/.gnupg/pubring.kbx:/gnupg/pubring.kbx:ro \
-v "$PWD":/data \
ghcr.io/0x77dev/pdf-sign sign --key 0xDEADBEEF document.pdf
Sigstore signing (interactive) — forward the OIDC callback port:
docker run --rm \
-p 127.0.0.1:8080:8080 -e OIDC_REDIRECT_PORT=8080 \
-v "$PWD":/data \
ghcr.io/0x77dev/pdf-sign sign --backend sigstore document.pdf
Sigstore signing (CI/non-interactive) — pass a pre-obtained identity token:
docker run --rm \
-e SIGSTORE_IDENTITY_TOKEN="$OIDC_TOKEN" \
-v "$PWD":/data \
ghcr.io/0x77dev/pdf-sign sign --backend sigstore document.pdf
Verify signatures:
docker run --rm -v "$PWD":/data \
ghcr.io/0x77dev/pdf-sign verify document_signed.pdf \
--certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com
sign - Sign a PDFUnified signing command with backend selection.
GPG backend (default):
pdf-sign sign contract.pdf --key 0xF1171FAAAA237211
# or explicitly:
pdf-sign sign --backend gpg contract.pdf --key user@example.com
Sigstore backend:
pdf-sign sign --backend sigstore document.pdf
Common options:
--output, -o: Output path (default: <input>_signed.pdf)--backend, -b: Backend to use (gpg or sigstore, default: gpg)--json: Machine-readable JSON outputGPG-specific options:
--key, -k: Key spec (file, fingerprint, key ID, or email) - required for GPG--embed-uid: Embed signer UID as notationSigstore-specific options:
--oidc-issuer <URL>: OIDC provider (default: https://oauth2.sigstore.dev/auth)--fulcio-url <URL>: Fulcio CA (default: https://fulcio.sigstore.dev)--rekor-url <URL>: Rekor log (default: https://rekor.sigstore.dev)--oidc-client-id <ID>: Client ID (default: sigstore)--oidc-client-secret <SECRET>: Client secret--identity-token <JWT>: Non-interactive (CI mode)--digest-algorithm <ALG>: Hash (default: sha512)verify - Verify signaturesAutomatically detects and verifies both GPG and Sigstore signatures in a single pass.
# Verify GPG signatures (uses keybox by default)
pdf-sign verify contract_signed.pdf
# Verify GPG with specific cert
pdf-sign verify contract_signed.pdf --cert signer.asc
# Verify Sigstore signatures (requires identity policy)
pdf-sign verify document_signed.pdf \
--certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com
# Verify both GPG and Sigstore in one PDF
pdf-sign verify multi_signed.pdf \
--cert alice.asc \
--certificate-identity bob@example.com \
--certificate-oidc-issuer https://accounts.google.com
GPG verification options:
--cert, -c: Optional cert spec (can repeat)Sigstore verification options:
--certificate-identity <EMAIL|URI>: Expected signer identity (required if Sigstore sigs present)--certificate-identity-regexp <REGEX>: Identity regex--certificate-oidc-issuer <URL>: Expected issuer (required if Sigstore sigs present)--certificate-oidc-issuer-regexp <REGEX>: Issuer regex--offline: Skip Rekor verificationCommon options:
--json: Machine-readable JSON outputchallenge - Prepare signing challenge for remote/air-gapped GPG signingCreate a challenge file for signing on a remote or air-gapped machine.
pdf-sign challenge document.pdf --key 0xDEADBEEF --output challenge.json
Options:
--key, -k: Key specification (required)--output, -o: Output path for challenge JSON (default: stdout)--embed-uid: Embed signer UID into signature--json: Machine-readable JSON outputChallenge format:
{
"version": 1,
"fingerprint": "ABCD1234...",
"data_base64": "SGVsbG8...",
"gpg_command": "echo 'SGVsbG8...' | base64 -d | gpg --detach-sign --armor -u 0xDEADBEEF > signature.asc",
"created_at": "2025-12-13T10:00:00Z",
"embed_uid": false
}
apply-response - Apply signature response from challenge-response workflowApply a signature created on a remote machine to complete the signing process.
pdf-sign apply-response document.pdf \
--challenge challenge.json \
--signature signature.asc \
--output signed.pdf
Options:
--challenge, -c: Path to challenge JSON file (required)--signature, -s: Path to signature file (.asc) (required)--output, -o: Output path for signed PDF (default: <input>_signed.pdf)--json: Machine-readable JSON outputgpg-agent.~/.gnupg/pubring.kbx for verification.sha512-<base64>).tracing instrumentation (never logs sensitive data).gpg-agent.Standard ASCII-armored blocks appended after %%EOF:
%%EOF
-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----
Versioned bilrost-encoded blocks with digest binding:
%%EOF
-----BEGIN PDF-SIGN SIGSTORE-----
<base64-encoded bilrost payload>
-----END PDF-SIGN SIGSTORE-----
Bilrost payload (v1):
version: Format version (1)signed_range_len: Length of clean PDF bytesdigest_alg: Hash algorithm (1 = SHA-512)digest: Raw digest bytes (for integrity binding)bundle_json: Sigstore bundle (signature + cert + Rekor proof)cargo)gpg-agentgpg-agent (software or hardware)cargo)--identity-token for CIGNUPGHOME: GPG keybox location (default: ~/.gnupg)RUST_LOG: Tracing verbosity (e.g., RUST_LOG=debug)SIGSTORE_IDENTITY_TOKEN: Pre-obtained OIDC identity token (JWT) for CI workflows (bypasses interactive browser flow)OIDC_REDIRECT_PORT: Local port for OIDC callback listener (default: OS-assigned dynamic port). Set to a fixed port (e.g., 8080) if you need predictable port forwarding or firewall rulesstderr: Progress, status, errorsstdout: Result paths (sign) or "OK" (verify) for pipelines# Sign with hardware key (default backend)
pdf-sign sign contract.pdf --key user@example.com
# Verify
pdf-sign verify contract_signed.pdf
# Interactive signing (opens browser for OIDC)
pdf-sign sign --backend sigstore document.pdf
# Verify with strict identity policy
pdf-sign verify document_signed.pdf \
--certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com
# Non-interactive signing with pre-obtained token
pdf-sign sign --backend sigstore release.pdf --identity-token "$OIDC_TOKEN"
# Verify in CI
pdf-sign verify release_signed.pdf \
--certificate-identity https://github.com/org/repo/.github/workflows/release.yml@refs/tags/v1.0.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--json
# Alice signs with GPG
pdf-sign sign contract.pdf --key alice@example.com
# Bob adds Sigstore signature to the same PDF
pdf-sign sign --backend sigstore contract_signed.pdf --output contract_multi.pdf
# Verify both signatures in one command
pdf-sign verify contract_multi.pdf \
--cert alice.asc \
--certificate-identity bob@example.com \
--certificate-oidc-issuer https://accounts.google.com
# 1. On connected machine: Prepare challenge
pdf-sign challenge sensitive.pdf --key 0xABCD1234 --output challenge.json
# 2. Transfer challenge.json to air-gapped machine
# 3. On air-gapped machine: Sign the challenge
cat challenge.json | jq -r '.data_base64' | base64 -d | \
gpg --detach-sign --armor -u 0xABCD1234 > signature.asc
# 4. Transfer signature.asc back to connected machine
# 5. On connected machine: Apply signature
pdf-sign apply-response sensitive.pdf \
--challenge challenge.json \
--signature signature.asc \
--output sensitive_signed.pdf
# 6. Verify the result
pdf-sign verify sensitive_signed.pdf
$ claude mcp add pdf-sign \
-- python -m otcore.mcp_server <graph>