Skip to main content

Cross-platform Python client for ARX CoSign electronic signatures via SOAP API

Project description

Revenant

revenant (Python)

CI pyright: strict coverage: 96% code style: ruff

Cross-platform Python client for ARX CoSign electronic signatures via SOAP API. No Windows required.

Server-specific settings (URL, TLS, identity discovery) are managed through server profiles — see src/revenant/config/profiles.py. EKENG-specific details are documented in ../docs/ekeng/. For SOAP API technical details, see ../docs/soap-api.md.

Download

Pre-built binaries are available on the Releases page:

Platform CLI GUI
macOS (Apple Silicon) revenant-cli-macos-arm64 Revenant-gui-macos-arm64.dmg
Linux (x64) revenant-cli-linux-x64 Revenant-x86_64.AppImage
Linux (ARM64) revenant-cli-linux-arm64 Revenant-aarch64.AppImage
Windows (x64) revenant-windows-x64.zip Revenant.msix

Quick start (macOS)

# Download and run CLI
curl -LO https://github.com/lobotomoe/revenant/releases/latest/download/revenant-cli-macos-arm64
chmod +x revenant-cli-macos-arm64
./revenant-cli-macos-arm64 setup

Quick start (Linux x64)

# CLI
curl -LO https://github.com/lobotomoe/revenant/releases/latest/download/revenant-cli-linux-x64
chmod +x revenant-cli-linux-x64
./revenant-cli-linux-x64 setup

# GUI (AppImage)
curl -LO https://github.com/lobotomoe/revenant/releases/latest/download/Revenant-x86_64.AppImage
chmod +x Revenant-x86_64.AppImage
./Revenant-x86_64.AppImage

Quick start (Linux ARM64)

# CLI
curl -LO https://github.com/lobotomoe/revenant/releases/latest/download/revenant-cli-linux-arm64
chmod +x revenant-cli-linux-arm64
./revenant-cli-linux-arm64 setup

# GUI (AppImage)
curl -LO https://github.com/lobotomoe/revenant/releases/latest/download/Revenant-aarch64.AppImage
chmod +x Revenant-aarch64.AppImage
./Revenant-aarch64.AppImage

Quick start (Windows)

Download revenant-windows-x64.zip from Releases, extract, and run:

Expand-Archive revenant-windows-x64.zip -DestinationPath revenant
.\revenant\revenant.exe setup

Alternatively, install the GUI via Revenant.msix (double-click to install).

Installation (pip)

# Install directly from GitHub
pip install "revenant @ git+https://github.com/lobotomoe/revenant.git#subdirectory=python"

# Or clone and install in editable mode (for development)
git clone https://github.com/lobotomoe/revenant
cd revenant/python
pip install -e "."                # core functionality (includes pikepdf)
pip install -e ".[secure]"        # + secure credential storage (keyring)
pip install -e ".[dev]"           # + development tools (pytest, ruff, pyright)

Uninstall

# PyPI
pip uninstall revenant

# Snap Store
sudo snap remove revenant

# Homebrew (macOS)
brew uninstall lobotomoe/revenant/revenant

Pre-built binaries: delete the downloaded file (revenant-cli-*, Revenant-*.AppImage, etc.).

macOS DMG: drag Revenant.app from /Applications to Trash.

Windows MSIX: Settings > Apps > Revenant > Uninstall.

Remove configuration and saved credentials:

revenant reset                # clears ~/.revenant/config.json

If you used keyring (secure credential storage), the password is stored in your system keychain. To remove it:

  • macOS: Keychain Access > search "revenant" > delete entry
  • Linux: secret-tool clear service revenant
  • Windows: Credential Manager > Windows Credentials > search "revenant" > remove

Quick start (library)

import revenant

# Sign with a built-in profile (handles URL, TLS, font automatically)
signed_pdf = revenant.sign(pdf_bytes, "user", "pass", profile="ekeng")

# Sign with a custom server URL
signed_pdf = revenant.sign(pdf_bytes, "user", "pass", url="https://server/SAPIWS/DSS.asmx")

# Use saved config (after `revenant setup`)
signed_pdf = revenant.sign(pdf_bytes, "user", "pass")

# Detached CMS/PKCS#7 signature
cms_der = revenant.sign_detached(pdf_bytes, "user", "pass", profile="ekeng")

All functions raise typed exceptions (AuthError, ServerError, TLSError). See Library usage for the full low-level API.

Usage

Initial setup

Configure your server, credentials, and signer identity:

revenant setup                    # interactive wizard
revenant setup --profile ekeng    # skip server selection

The setup wizard walks you through:

  1. Choose server — pick a built-in profile (e.g. EKENG) or enter a custom URL
  2. Ping server — verify the endpoint is reachable (WSDL fetch, no auth)
  3. Enter credentials — with lockout warnings for profiles that have them
  4. Discover identity — automatically extracts your name/email/org from the server's signing certificate. Falls back to signed PDF extraction or manual entry.
  5. Save — writes everything to ~/.revenant/config.json

You can re-run revenant setup at any time to reconfigure.

GUI

A graphical interface is available via tkinter (Python stdlib):

revenant gui          # if installed via pip
revenant-gui          # alternative entry point
python -m revenant gui

The GUI provides file pickers for PDF/image/output, credential fields, position/page selectors, and a Sign button.

Requires tkinter — if missing, the tool shows platform-specific install instructions (e.g. brew install python-tk@3.13 on macOS).

Sign a PDF (embedded — default)

# Embedded signature — produces document_signed.pdf
revenant sign document.pdf

# Custom output path
revenant sign document.pdf -o signed.pdf

# Sign multiple files
revenant sign *.pdf

# Detached .p7s signature instead
revenant sign document.pdf --detached

# Preview what would be done (no actual signing)
revenant sign document.pdf --dry-run

# Specify page and position
revenant sign document.pdf --page 1 --position top-left
revenant sign document.pdf --page last --position bottom-center

# Add signature image (PNG or JPEG, scaled to fit field, left side)
revenant sign document.pdf --image signature.png

# Armenian-script signature appearance
revenant sign document.pdf --font ghea-grapalat

Page numbering: CLI uses 1-based pages (--page 1 = first page). Use first, last, or a number.

Fonts: noto-sans (default, Latin/Cyrillic), ghea-grapalat (Armenian), ghea-mariam (Armenian serif). The EKENG profile defaults to ghea-grapalat.

Signature image: PNG or JPEG. The image is scaled proportionally to fit the left side of the signature field. Recommended: transparent PNG, around 200x100px.

Verify a signature

# Verify using CA root cert (auto-detected from certs/ directory)
revenant verify document.pdf

# Specify signature file explicitly
revenant verify document.pdf -s document.pdf.p7s

Check an embedded signature

revenant check signed.pdf

Inspect a detached signature

revenant info document.pdf.p7s

Manage configuration

revenant logout   # clear credentials + identity, keep server config
revenant reset    # clear everything (server, credentials, identity)

logout preserves the server URL and profile so you can re-authenticate with revenant setup without reconfiguring the server. reset removes all configuration from ~/.revenant/config.json.

Output modes

By default, the tool produces embedded PDF signatures (visible signature field in the PDF). Use --detached for detached CMS/PKCS#7 .p7s files.

Embedded signing uses a true incremental update — the original PDF bytes are preserved exactly, with signature objects appended after %%EOF. pikepdf is used read-only (page dimensions, object graph) and never rewrites the PDF.

Embedded signing includes automatic post-sign verification — after inserting the CMS, the tool re-reads the PDF and confirms the ByteRange hash matches what was sent to the server. If verification fails, the file is not saved.

Embedded signatures include a visual appearance stream — fields configured per server profile (name, ID, date, etc.) are stacked vertically. With an optional signature image, the image appears on the left. Configure signer identity via revenant setup.

The detached .p7s file can be verified with:

  • openssl cms -verify -inform DER -in doc.pdf.p7s -content doc.pdf -CAfile certs/ca_root.pem
  • Any PKCS#7/CMS-compatible verification tool

Library usage

The package can be used as a Python library — the CLI is just a thin wrapper.

import revenant
from revenant.network.soap_transport import SoapSigningTransport

transport = SoapSigningTransport("https://ca.gov.am:8080/SAPIWS/DSS.asmx")

# Embedded signature (visible field in the PDF, requires pikepdf)
# Library uses 0-based pages (page=0 = first page)
signed_pdf = revenant.sign_pdf_embedded(
    pdf_bytes, transport, "user", "pass", timeout=120,
    page=0, x=350, y=50, w=200, h=70,
    name="Signer Name", reason="Approved",
)

# Detached CMS/PKCS#7 signature
cms_der = revenant.sign_pdf_detached(pdf_bytes, transport, "user", "pass")

# Sign a raw SHA-1 hash (for custom workflows)
import hashlib
digest = hashlib.sha1(data).digest()
cms_der = revenant.sign_hash(digest, transport, "user", "pass")

# Sign arbitrary data (server computes the hash)
cms_der = revenant.sign_data(raw_bytes, transport, "user", "pass")

Verification

# Verify the last embedded signature
result = revenant.verify_embedded_signature(signed_pdf)
print(result.valid, result.details)

# Verify ALL signatures in a multi-signed PDF
results = revenant.verify_all_embedded_signatures(signed_pdf)
for r in results:
    print(r.signer, r.valid)

Signature positioning

# Available position presets
print(revenant.POSITION_PRESETS)
# {'bottom-right', 'top-right', 'bottom-left', 'top-left', 'bottom-center'}

# Resolve aliases (e.g. "br" -> "bottom-right")
pos = revenant.resolve_position("br")

Signature options

EmbeddedSignatureOptions bundles all appearance and positioning parameters:

from revenant import EmbeddedSignatureOptions

opts = EmbeddedSignatureOptions(
    page="last",                 # 0-based int, "first", or "last"
    position="bottom-right",     # preset name (ignored when x/y are set)
    x=350, y=50,                 # manual coordinates (PDF points, origin=bottom-left)
    w=200, h=70,                 # field dimensions in PDF points
    reason="Approved",           # signature reason string
    name="Signer Name",         # signer display name
    image_path="sig.png",        # optional PNG/JPEG signature image
    fields=["Name", "Date"],     # custom appearance field strings
    visible=True,                # False for invisible signatures
    font="noto-sans",            # "noto-sans", "ghea-grapalat", or "ghea-mariam"
)

signed = revenant.sign_pdf_embedded(pdf, transport, user, pw, options=opts)

Utilities

# Get configured signer name (from ~/.revenant/config.json)
name = revenant.get_signer_name()  # returns str | None

Error handling

All functions raise typed exceptions from a hierarchy rooted at RevenantError:

RevenantError (base)
├── AuthError           -- wrong credentials, account locked
├── ServerError         -- server returned an error response
├── TLSError            -- connection/TLS issues (.retryable flag)
├── PDFError            -- invalid PDF structure, parse failures
├── ConfigError         -- missing or malformed configuration
└── CertificateError    -- certificate parsing/extraction errors
from revenant import AuthError, ServerError, TLSError, PDFError

try:
    revenant.sign_pdf_embedded(pdf, transport, user, password)
except AuthError:
    print("Wrong credentials or account locked")
except TLSError as e:
    if e.retryable:
        print("Transient connection error, retry later")
    else:
        print(f"TLS configuration issue: {e}")
except ServerError as e:
    print(f"Server error: {e}")
except PDFError as e:
    print(f"Invalid PDF: {e}")

API stability

This project follows semver. The public API (revenant.__all__) is stable from 1.0. Pre-1.0 releases may have breaking changes between minor versions.

Server profiles

Server-specific settings are managed through ServerProfile objects. The EKENG profile is built-in; custom servers are created at setup time.

Built-in profile: EKENG

  • URL: https://ca.gov.am:8080/SAPIWS/DSS.asmx
  • TLS: Legacy TLSv1.0 / RC4-MD5 (auto-detected)
  • Account lockout: 5 failed attempts
  • Font: ghea-grapalat (Armenian)
  • Identity: extracted from signing certificate (name, SSN, email)

Custom servers

Run revenant setup and choose "Custom URL" to configure any CoSign server. The tool auto-detects whether the server requires legacy TLS on first connection.

ServerProfile fields (defined in src/revenant/config/profiles.py):

Field Type Description
name str Profile identifier
url str SOAP endpoint URL (HTTPS only)
timeout int Request timeout in seconds (default: 120)
legacy_tls bool Force TLSv1.0/RC4 mode (default: auto-detect)
identity_methods tuple Discovery methods: "server", "manual"
ca_cert_markers tuple Strings to identify CA certificates for filtering
max_auth_attempts int Lockout threshold (0 = no lockout warning)
cert_fields tuple Certificate fields for identity extraction
sig_fields tuple Fields for signature visual appearance
font str Default font for signatures

Prerequisites

  • Python 3.10+
  • pikepdf — for embedded PDF signatures (brings in qpdf, Pillow, lxml)
  • asn1crypto — certificate parsing (PKCS#7, X.509)
  • tlslite-ng — legacy TLS for servers requiring TLS 1.0 / RC4 (e.g. EKENG)
  • defusedxml — safe XML parsing for SOAP responses
  • openssl for the verify command (optional, for detached signature verification)
  • CoSign credentials (username + password)
  • Network access to the CoSign server

All Python dependencies are installed automatically via pip install.

Platform notes

macOS Linux Windows
Signing (sign) works out of the box works out of the box works out of the box
Embedded PDF pikepdf pikepdf pikepdf
GUI (revenant gui) brew install python-tk apt install python3-tk included with Python
verify openssl included openssl included requires OpenSSL install

The core Python code is fully cross-platform. TLS is handled transparently: standard servers use system HTTPS (urllib), while legacy servers (e.g. EKENG with TLSv1.0/RC4) are handled via tlslite-ng (pure Python, no native dependencies). The transport layer auto-detects TLS mode per host on first connection. See ../docs/ekeng/ for EKENG-specific details.

Credentials

Credentials are resolved in this order (first match wins):

  1. Environment variables REVENANT_USER / REVENANT_PASS
  2. System keychain via keyring (if installed)
  3. Saved config in ~/.revenant/config.json (saved during revenant setup or after first successful sign)
  4. Interactive prompt (if none of the above)

After a successful signing from an interactive prompt, the tool offers to save credentials for future use.

Secure storage (recommended)

Install with keyring support for secure credential storage:

pip install revenant[secure]
# or
pip install keyring

When keyring is installed, passwords are stored in your system's secure credential store:

  • macOS: Keychain
  • Linux: Secret Service (GNOME Keyring, KWallet)
  • Windows: Windows Credential Manager

The username is still saved in ~/.revenant/config.json, but the password is stored securely in the system keychain.

Fallback (plaintext)

If keyring is not installed, credentials are stored in ~/.revenant/config.json (permissions 0600). You will see a warning when saving credentials without keyring:

WARNING: Password is stored in PLAINTEXT (file is chmod 600)
For secure storage, install: pip install keyring

To clear saved credentials, remove username/password from the config file or delete it.

Environment variables

Variable Description
REVENANT_USER CoSign username (overrides saved config)
REVENANT_PASS CoSign password (overrides saved config)
REVENANT_URL SOAP endpoint (overrides profile URL from revenant setup)
REVENANT_TIMEOUT Request timeout in seconds (default: 120)
REVENANT_NAME Signer display name (overrides config from revenant setup)

Development

cd python/
pip install -e ".[dev]"     # install with dev tools (pytest, ruff, pyright)
pytest                      # run unit tests (no server needed)
ruff check src/             # lint
ruff format src/            # format
pyright src/                # type check

# Integration tests (require live server + credentials)
REVENANT_USER=... REVENANT_PASS=... pytest -m integration

Building from source

Build standalone binaries (CLI + GUI) from the Python source. Each platform uses a different toolchain.

macOS (.app + DMG)

Uses py2app for a sandbox-compatible .app bundle.

cd python/

# Install build dependencies
pip install -e ".[build-mac]"

# Optional: install create-dmg for a fancy DMG layout (Applications link, background image)
brew install create-dmg

# Build .app bundle + DMG
python scripts/build.py mac

# Build .app only (no DMG -- useful if you want to sign before creating the DMG)
python scripts/build.py mac --no-dmg

# Create DMG from an existing .app
python scripts/build.py dmg

Requires: Python 3.10+, tkinter (brew install python-tk@3.13), Xcode Command Line Tools.

Output: dist/Revenant.app, dist/Revenant.dmg

Linux (CLI + GUI + AppImage)

Uses PyInstaller for standalone one-file binaries.

cd python/

# Install build dependencies
pip install -e ".[build]"

# Build GUI + CLI binaries (runs in parallel)
python scripts/build.py all

# Build only CLI or GUI
python scripts/build.py cli
python scripts/build.py gui

# Create AppImage from the GUI binary (requires Linux)
python scripts/build_appimage.py

Requires: Python 3.10+, tkinter (apt install python3-tk for GUI).

Output: dist/revenant (CLI), dist/revenant-gui (GUI), dist/Revenant-{arch}.AppImage (e.g. Revenant-x86_64.AppImage, Revenant-aarch64.AppImage)

Windows (CLI + GUI + MSIX)

Uses Nuitka for standalone executables (avoids Windows Defender false positives from PyInstaller's extract-to-temp pattern).

cd python

# Install build dependencies
pip install -e ".[build-win]"

# Build GUI + CLI (sequential -- Nuitka shares a download cache)
python scripts/build.py all

# Build only CLI or GUI
python scripts/build.py cli
python scripts/build.py gui

# Create MSIX package (requires Windows SDK for makeappx.exe)
python scripts/build_msix.py

Requires: Python 3.10+, tkinter (included with Python on Windows), Windows SDK (for MSIX only).

Output: dist/revenant-standalone/ (folder with revenant.exe + revenant-gui.exe), dist/Revenant.msix

EKENG-specific notes

EKENG-specific behavior is documented in ../docs/ekeng/ and configured as the ekeng profile in src/revenant/config/profiles.py.

Key points:

  • Server: ca.gov.am:8080 (TLSv1.0 / RC4-MD5)
  • Account lockout after 5 failed attempts
  • Signatures are accepted by the EKENG validator (ekeng.am) and e-request (e-request.am)

Troubleshooting

AuthError: Authentication failed -- Wrong username or password. If using EKENG, the account locks after 5 failed attempts. Wait or contact your administrator.

TLSError: ... -- Server unreachable or TLS version mismatch. Check network access to the server. For EKENG, the server requires TLSv1.0/RC4 which is handled automatically by tlslite-ng.

ServerError: ... -- The server rejected the request. Common causes: expired certificate, server maintenance, or unsupported document format.

PDFError: ... -- The PDF is malformed, encrypted, or not a valid PDF file. Try re-saving the PDF from a different application.

Signature appearance looks wrong -- Run revenant setup to reconfigure your signer identity. The signature fields (name, ID, date) come from the server profile configuration.

Validator rejects the signed PDF -- See ../docs/ekeng/ for EKENG validator requirements. Common issues: missing /Info dictionary in the incremental update, or modified PDF bytes after signing.

Known limitations

  • SHA-1 only — the server rejects SHA-256. This is a server-side limitation.
  • Non-standard CMS OIDs — the server returns sha1WithRSAEncryption as digestAlgorithm (wrong per RFC 5652). See ../docs/verification.md.
  • No timestamp (TSA) — the WSDL defines timestamp options but the server ignores them.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

revenant-0.2.1.tar.gz (321.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

revenant-0.2.1-py3-none-any.whl (281.6 kB view details)

Uploaded Python 3

File details

Details for the file revenant-0.2.1.tar.gz.

File metadata

  • Download URL: revenant-0.2.1.tar.gz
  • Upload date:
  • Size: 321.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for revenant-0.2.1.tar.gz
Algorithm Hash digest
SHA256 aa2937ae6536ebebc28c816a9eeeff9a9ebc5ebce2592281147514b8c160c7cf
MD5 73e7f8dfe83785ae9fe7885f9d011af0
BLAKE2b-256 8196680304d46d0400bf5d0d7ae3d06259f1e142c56468cada6d8901aec9822e

See more details on using hashes here.

Provenance

The following attestation bundles were made for revenant-0.2.1.tar.gz:

Publisher: release.yml on lobotomoe/revenant

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file revenant-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: revenant-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 281.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for revenant-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7bb0e0cf357c4539d9bc24bd9a829e79ec2624bb499e447b0acc6a804990c94a
MD5 434a35527931313bffb865e03873bd16
BLAKE2b-256 a8416acaa8f005877175278855ca4ed3a61c6ea91f6e0956c466fd5340e62f3c

See more details on using hashes here.

Provenance

The following attestation bundles were made for revenant-0.2.1-py3-none-any.whl:

Publisher: release.yml on lobotomoe/revenant

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page