npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@certysign/sdk

v2.4.0

Published

Official Node.js SDK for CertySign — hash-based digital signing, X.509 certificates, and PKI services. Documents never leave your system.

Downloads

271

Readme

@certysign/sdk

Official Node.js SDK for CertySign Trust Services — hash-based digital signing, multi-recipient signing sessions, X.509 certificate management, and PKI operations built for East Africa.

Documents never leave your infrastructure. The SDK hashes documents locally, sends only the cryptographic hash to CertySign for HSM-backed signing, receives the CMS/PKCS#7 signature, and embeds it into PDF/XML/JSON — all on your system.


Table of Contents


Installation

npm install @certysign/sdk

Node.js >= 18 required.

Peer dependencies (installed automatically):

  • pdf-lib — PDF manipulation for visual stamps and /Sig dictionaries
  • node-forge — PKCS#7/CMS ASN.1 construction for cryptographic embedding

Quick Start

PAdES Signing — Cryptographically Valid PDF Signatures (Recommended)

This is the recommended approach for v2.2.0+. It produces PAdES Baseline B-T PDF signatures with embedded RFC 3161 timestamps, recognised as valid by Adobe Reader, Foxit Reader, and other PDF validators. The TSA URL is resolved automatically from your environment — no configuration needed.

const { CertySignClient } = require('@certysign/sdk');
const fs = require('fs');

const client = new CertySignClient({
  publicKey:   process.env.CERTYSIGN_PUBLIC_KEY,   // cs_pk_...
  secretKey:   process.env.CERTYSIGN_SECRET_KEY,   // cs_sk_...
  environment: 'production'   // 'staging' | 'development'
});

const pdfBuffer = fs.readFileSync('./claim.pdf');

// Get the active certificate for the visual stamp
const cert = await client.certificates.getActive();

// One call does everything:
//   1. Prepares PDF with visual stamp + /Sig placeholder
//   2. Computes SHA-256 hash of the ByteRange regions
//   3. Calls your signCallback to sign the hash via HSM
//   4. Builds PKCS#7 SignedData and patches into /Contents
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
  certSerialNumber: cert.data?.serialNumber,
  signerEmail:      '[email protected]',
  signerName:       'Dr. Amina Okonkwo',
  reason:           'Health claim approval — DHA Kenya',
  location:         'Nairobi, Kenya',
  signCallback: async (byteRangeHash) => {
    // The embedder computed the ByteRange hash — now sign it via HSM
    return client.sign.signHash({
      documentHash:  byteRangeHash,
      hashAlgorithm: 'sha256',
      fileName:      'claim.pdf',
      reason:        'Health claim approval'
    });
  }
});

fs.writeFileSync('./claim-signed.pdf', signedPdf);
// Open in Adobe Reader → Signature Panel shows:
//   ✓ Valid digital signature
//   ✓ Timestamp: RFC 3161 (proves when it was signed)
//   ✓ Standard: PAdES Baseline B-T

Simple Signing — Hash → Sign → Embed (Legacy approach)

This approach signs the document hash first, then embeds the pre-computed signature. The PDF will have visual stamps and PKCS#7 structure, but the signature covers the original document hash rather than the ByteRange, so PDF readers may show a warning.

const pdfBuffer = fs.readFileSync('./claim.pdf');

// 1. Hash locally & sign remotely
const result = await client.sign.hashAndSign({
  document:   pdfBuffer,
  fileName:   'claim.pdf',
  signerName: 'Dr. Amina Okonkwo',
  reason:     'Health claim approval'
});

// 2. Embed signature into PDF
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
  signature:       result.data.signature,
  certificate:     result.data.certificate,     // PEM string
  certSerialNumber: result.data.certSerialNumber,
  chain:           result.data.chain,            // PEM chain string
  signerName:      'Dr. Amina Okonkwo',
  reason:          'Health claim approval',
  documentHash:    result.data.documentHash,
  hashAlgorithm:   result.data.hashAlgorithm,
  algorithm:       result.data.algorithm
});

fs.writeFileSync('./claim-signed.pdf', signedPdf);

Multi-recipient signing with OTP verification

// 1. Hash documents locally
const { hash } = client.hasher.hash(pdfBuffer, 'sha256');

// 2. Create a signing session with recipients
const session = await client.sessions.create({
  name: 'Q1 Financial Report Approval',
  documents: [{
    documentId: 'doc-q1-report',
    fileName:   'Q1-report.pdf',
    hash,
    hashAlgorithm: 'sha256'
  }],
  recipients: [
    { email: '[email protected]', name: 'CFO', role: 'signer', order: 1 },
    { email: '[email protected]', name: 'CEO', role: 'signer', order: 2 }
  ],
  signingOrder: 'sequential'   // CFO signs first, then CEO
});

// 3. Send OTP to recipient
await client.sessions.sendOtp(session.data.session._id, recipientId);

// 4. Verify OTP (recipient enters code from email)
const { data } = await client.sessions.verifyOtp(session.data.session._id, recipientId, '485721');

// 5. Recipient signs with their token
const signed = await client.sessions.recipientSign(
  session.data.session._id,
  recipientId,
  data.signingToken
);

Architecture

PAdES Flow (Recommended — signCallback)

┌──────────────────────────────────────────────────────────────────┐
│                    SUBSCRIBER'S INFRASTRUCTURE                    │
│                                                                    │
│  ┌───────────┐                                                     │
│  │ PDF       │                                                     │
│  │ Document  │─────────────┐                                       │
│  └───────────┘             ▼                                       │
│              ┌──────────────────────────────┐                      │
│              │  embedInPdf(pdf, {           │                      │
│              │    signCallback: async (h) => │   1. Prepare PDF     │
│              │      client.sign.signHash()   │      + visual stamp  │
│              │  })                           │      + /Sig dict     │
│              └──────────┬───────────────────┘                      │
│                         │ 2. SHA-256(ByteRange)                    │
│                         ▼                                          │
│              ┌───────────────────────────────────┐                 │
│              │        CertySign API               │                 │
│              │  POST /sdk/v1/sign/hash            │                 │
│              │  ┌─────────────────────────────┐   │                 │
│              │  │  HSM (Google Cloud KMS)      │   │                 │
│              │  │  Signs ByteRange hash        │   │                 │
│              │  └─────────────────────────────┘   │                 │
│              └───────────────┬───────────────────┘                 │
│                              │ RSA signature + cert PEM + chain    │
│                              ▼                                     │
│              ┌──────────────────────────────┐                      │
│              │  3. Build PKCS#7 SignedData   │                      │
│              │  4. Patch into /Contents      │                      │
│              │  5. Patch /ByteRange          │                      │
│              └──────────────┬───────────────┘                      │
│                             ▼                                      │
│              ┌──────────────────────────────┐                      │
│              │  PAdES-Signed PDF             │                      │
│              │  ✓ Adobe Reader validates     │                      │
│              │  ✓ /Sig + /ByteRange + PKCS#7 │                      │
│              │  ✓ RFC 3161 timestamp (TSA)    │                      │
│              │  ✓ Stays on YOUR system       │                      │
│              └──────────────────────────────┘                      │
│                                                                    │
└──────────────────────────────────────────────────────────────────┘

What crosses the network: Only the SHA-256 hash of the PDF ByteRange (64 hex characters). What stays local: The original document, the signing preparation, the PKCS#7 construction, and the final signed PDF.


Authentication

API keys are managed in the CertySign portal under Settings → API Keys.

Each key pair consists of:

| Value | Header | Format | |-------|--------|--------| | Public key | X-API-Key-Id | cs_pk_<48 hex chars> | | Secret key | X-API-Key-Secret | cs_sk_<64 hex chars> |

The secret key is shown once at creation. Store it in a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) — never in source code.


Environments

| Environment | Base URL | TSA URL | |-------------|----------|----------| | production | https://core.certysign.io | https://tsa.certysign.io | | staging | https://service.certysign.io | https://tsa-staging.certysign.io | | development | http://localhost:8000 | http://localhost:5015 |

The TSA URL is resolved automatically from your environment — no manual configuration needed. Every PDF signature includes an RFC 3161 timestamp proving when the signature was created. Override with the tsaUrl constructor option if needed.

// TSA is automatic — just set your environment
const client = new CertySignClient({
  publicKey:   process.env.CERTYSIGN_PUBLIC_KEY,
  secretKey:   process.env.CERTYSIGN_SECRET_KEY,
  environment: 'production'  // TSA → https://tsa.certysign.io (automatic)
});

// Or override TSA URL explicitly
const client = new CertySignClient({
  publicKey:   process.env.CERTYSIGN_PUBLIC_KEY,
  secretKey:   process.env.CERTYSIGN_SECRET_KEY,
  environment: 'production',
  tsaUrl:      'https://custom-tsa.example.com'  // Custom TSA endpoint
});

API Resources

client.sign — Hash-Based Signing

v2 (default): Documents are hashed locally. Only the hash is sent to CertySign.
For the legacy file-upload flow, see client.legacySign.

hashAndSign(options) — Hash locally & sign in one call

const result = await client.sign.hashAndSign({
  document:   Buffer | string,      // Required — document content
  fileName:   'claim.pdf',          // Optional — default: 'document.pdf'
  mimeType:   'application/pdf',    // Optional — auto-detected from fileName
  signerName: 'Dr. Amina Okonkwo', // Optional
  reason:     'Approval',           // Optional
  location:   'Nairobi, Kenya',     // Optional
  metadata:   { claimId: '...' }    // Optional
});
// result.data.signature           — base64-encoded raw RSA signature
// result.data.documentHash        — hex hash of the document
// result.data.hashAlgorithm       — 'sha256' | 'sha384' | 'sha512'
// result.data.algorithm           — 'SHA256withRSA' etc.
// result.data.certificate         — signer certificate PEM string
// result.data.chain               — full trust chain PEM string
// result.data.certSerialNumber    — certificate serial (hex)
// result.data.certFingerprint     — SHA-256 fingerprint (hex)
// result.data.timestamp           — ISO 8601 timestamp

signHash(options) — Sign a pre-computed hash

Use this when you already computed the hash yourself.

const result = await client.sign.signHash({
  documentHash:   'a3f2b8c1d4e5...',  // Required — hex-encoded hash
  hashAlgorithm:  'sha256',            // Required — 'sha256' | 'sha384' | 'sha512'
  fileName:       'report.pdf',        // Optional
  signerName:     'NHIF System',       // Optional
  reason:         'Automated signing', // Optional
  location:       'Nairobi',           // Optional
  metadata:       { batchId: '...' }   // Optional
});
// Same response shape as hashAndSign

batchHashAndSign(options) — Hash & sign multiple documents

const result = await client.sign.batchHashAndSign({
  documents: [
    { document: fs.readFileSync('./invoice-1.pdf'), fileName: 'invoice-1.pdf' },
    { document: fs.readFileSync('./invoice-2.pdf'), fileName: 'invoice-2.pdf' }
  ],
  signerName: 'NHIF Finance System',
  reason:     'Batch provider reimbursement'
});
// result.data.results[]            — array of signed results
// result.data.results[].documentHash
// result.data.results[].signature
// result.data.certificate           — shared certificate PEM for the batch
// result.data.certSerialNumber      — certificate serial (hex)
// result.data.chain                 — chain PEM

batchSignHashes(options) — Sign multiple pre-computed hashes

const result = await client.sign.batchSignHashes({
  documents: [
    { documentHash: 'a3f2...', hashAlgorithm: 'sha256', fileName: 'doc1.pdf' },
    { documentHash: 'b7e1...', hashAlgorithm: 'sha256', fileName: 'doc2.pdf' }
  ],
  signerName: 'Batch System',
  reason:     'Monthly invoices'
});
// Up to 50 documents per batch

verifyById(envelopeId) — Verify a signed envelope

const result = await client.sign.verifyById('env_abc123');
// result.data.valid           true/false
// result.data.signerName
// result.data.signedAt

client.sessions — Multi-Recipient Signing Sessions

Signing sessions support multi-recipient workflows with OTP email verification. Documents are represented by their hashes — the actual files never leave your system.

Signing Order

  • sequential — recipients sign in the order specified by their order field. Recipient 2 cannot sign until recipient 1 has completed.
  • parallel — all recipients can sign independently in any order.

create(options) — Create a signing session

const session = await client.sessions.create({
  name: 'Q1 Board Resolution',                    // Required
  documents: [                                      // Required — at least one
    {
      documentId:    'doc-resolution',              // Required — your unique ID
      fileName:      'board-resolution.pdf',        // Required
      hash:          'a3f2b8c1d4e5f6...',          // Required — hex hash
      hashAlgorithm: 'sha256',                      // Required
      mimeType:      'application/pdf'              // Optional
    }
  ],
  recipients: [                                     // Required — at least one
    {
      email: '[email protected]',
      name:  'Board Chair',
      role:  'signer',          // 'signer' | 'viewer' | 'approver'
      order: 1
    },
    {
      email: '[email protected]',
      name:  'Company Secretary',
      role:  'signer',
      order: 2
    }
  ],
  signingOrder: 'sequential',   // 'sequential' | 'parallel'
  expiresAt: '2026-02-01T00:00:00Z'  // Optional — default: 7 days
});
// session.data.session._id
// session.data.session.status       — 'active'
// session.data.session.recipients[] — each has .recipientId

get(sessionId) — Get session details

const { data } = await client.sessions.get(sessionId);
// data.session.status              — 'active' | 'completed' | 'expired'
// data.session.documents[]         — includes .signature after signing
// data.session.recipients[].status — 'pending' | 'otp_sent' | 'verified' | 'signed'

list(options) — List signing sessions

const { data } = await client.sessions.list({
  page:   1,
  limit:  20,
  status: 'active'   // Optional filter
});
// data.sessions[]
// data.pagination.total

sendOtp(sessionId, recipientId) — Send OTP email

Sends a 6-digit OTP code to the recipient's email address. The OTP is valid for 10 minutes. Respects signing order — in sequential mode, you cannot send OTP to recipient 2 until recipient 1 has signed.

await client.sessions.sendOtp(sessionId, recipientId);
// OTP email sent to recipient

verifyOtp(sessionId, recipientId, code) — Verify OTP

Validates the OTP code. After 5 failed attempts, the OTP is locked and must be re-sent. Returns a signing token valid for 30 minutes.

const { data } = await client.sessions.verifyOtp(sessionId, recipientId, '485721');
// data.signingToken   — use this for the signing step
// data.expiresAt      — token expiry (30 minutes)

recipientSign(sessionId, recipientId, signingToken) — Recipient signs

Signs all document hashes in the session using the tenant's HSM-backed certificate. The signing token must be valid and unexpired.

const { data } = await client.sessions.recipientSign(sessionId, recipientId, signingToken);
// data.session.status               — 'completed' if all recipients have signed
// data.session.documents[].signature — base64 CMS signature for each document
// data.session.documents[].signedAt
// data.session.documents[].signedBy

Full Signing Session Flow

const fs = require('fs');
const { CertySignClient, DocumentHasher } = require('@certysign/sdk');

const client = new CertySignClient({ publicKey, secretKey });
const hasher = new DocumentHasher();

// 1. Hash documents locally
const doc1 = fs.readFileSync('./contract.pdf');
const doc2 = fs.readFileSync('./addendum.pdf');
const hash1 = hasher.hash(doc1, 'sha256');
const hash2 = hasher.hash(doc2, 'sha256');

// 2. Create session
const { data: { session } } = await client.sessions.create({
  name: 'Service Contract Signing',
  documents: [
    { documentId: 'contract', fileName: 'contract.pdf', hash: hash1.hash, hashAlgorithm: 'sha256' },
    { documentId: 'addendum', fileName: 'addendum.pdf', hash: hash2.hash, hashAlgorithm: 'sha256' }
  ],
  recipients: [
    { email: '[email protected]', name: 'Vendor Rep', role: 'signer', order: 1 },
    { email: '[email protected]', name: 'Client Rep', role: 'signer', order: 2 }
  ],
  signingOrder: 'sequential'
});

// 3. First recipient: send OTP → verify → sign
const recipientId = session.recipients[0].recipientId;
await client.sessions.sendOtp(session._id, recipientId);

// (Recipient enters code from their email)
const { data: { signingToken } } = await client.sessions.verifyOtp(
  session._id, recipientId, '123456'
);
await client.sessions.recipientSign(session._id, recipientId, signingToken);

// 4. Second recipient follows the same flow...

// 5. Retrieve completed session and embed signatures
const completed = await client.sessions.get(session._id);
for (const doc of completed.data.session.documents) {
  const originalBuffer = doc.documentId === 'contract' ? doc1 : doc2;
  const signed = await client.embedder.embedInPdf(originalBuffer, {
    signature:      doc.signature,
    certificate:    completed.data.session.certSerialNumber,
    documentHash:   doc.hash,
    hashAlgorithm:  doc.hashAlgorithm
  });
  fs.writeFileSync(`./signed-${doc.fileName}`, signed);
}

client.hasher — Local Document Hashing

All hashing is done locally on your machine. No data is sent to CertySign.

hash(data, algorithm) — Hash a buffer or string

const { hash, algorithm, size } = client.hasher.hash(
  fs.readFileSync('./document.pdf'),
  'sha256'   // Optional — default: 'sha256'. Also: 'sha384', 'sha512'
);
// hash       — hex-encoded hash string
// algorithm  — 'sha256'
// size       — input size in bytes

hashFile(filePath, algorithm) — Hash a file by path (streaming)

Memory-efficient for large files — reads with streaming instead of loading the entire file.

const { hash, algorithm, size } = await client.hasher.hashFile(
  '/path/to/large-report.pdf',
  'sha256'
);

hashMany(documents, algorithm) — Batch hash buffers

const results = client.hasher.hashMany([
  { data: fs.readFileSync('./doc1.pdf'), fileName: 'doc1.pdf' },
  { data: fs.readFileSync('./doc2.pdf'), fileName: 'doc2.pdf' }
], 'sha256');
// results[] — each: { fileName, hash, algorithm, size }

hashFiles(filePaths, algorithm) — Batch hash file paths

const results = await client.hasher.hashFiles([
  '/path/to/doc1.pdf',
  '/path/to/doc2.pdf'
], 'sha256');
// results[] — each: { filePath, fileName, hash, algorithm, size }

Supported algorithms: sha256, sha384, sha512


client.embedder — Local Signature Embedding

Embeds CMS/PKCS#7 signatures into documents locally on your machine. PDF embedding creates cryptographically valid PAdES signatures recognised by Adobe Reader and Foxit.

embedInPdf(pdfBuffer, options) — Embed PAdES signature into PDF

Creates a visual signature stamp and a proper PDF /Sig dictionary with PKCS#7/CMS cryptographic embedding. The resulting PDF passes validation in Adobe Reader, Foxit Reader, and other PDF signature validators.

Recommended: signCallback approach (PAdES Baseline B-T)

The signCallback approach produces cryptographically valid PAdES B-T signatures with embedded RFC 3161 timestamps:

  1. The embedder prepares the PDF with visual stamp and an empty /Sig placeholder
  2. It computes the SHA-256 hash of the PDF's ByteRange regions
  3. Your signCallback sends that hash to CertySign's HSM for signing
  4. The embedder fetches an RFC 3161 timestamp from the TSA (automatic)
  5. The embedder builds a PKCS#7 SignedData structure with the timestamp as an unsigned attribute and patches it into /Contents
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
  signerEmail:      '[email protected]',          // Shown on visual stamp
  signerName:       'Mathews Ndoli',                   // Fallback if no email
  certSerialNumber: '9EF9C8E88478FC08C6759942274EB8A2', // Shown on visual stamp
  reason:           'Document approval',                // Shown on stamp + /Sig dict
  location:         'Nairobi, Kenya',                   // Optional
  signCallback: async (byteRangeHash) => {
    // byteRangeHash is a hex-encoded SHA-256 of the ByteRange regions
    return client.sign.signHash({
      documentHash:  byteRangeHash,
      hashAlgorithm: 'sha256',
      fileName:      'document.pdf'
    });
    // Must return: { data: { signature, certificate, chain, certSerialNumber } }
  }
});
// Returns: Buffer — PAdES-signed PDF
fs.writeFileSync('./signed.pdf', signedPdf);

Alternative: Pre-computed signature (legacy)

You can pass a pre-computed signature instead of signCallback. The signature will be embedded in a PKCS#7 structure, but since it was computed over the original document hash (not the ByteRange), PDF readers may not fully validate it.

const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
  signature:       result.data.signature,        // Base64 raw RSA signature
  certificate:     result.data.certificate,      // PEM string
  chain:           result.data.chain,            // PEM chain string
  certSerialNumber: result.data.certSerialNumber,
  signerEmail:     '[email protected]',
  reason:          'Approval'
});

Signature Position:

const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
  // ... signCallback or signature ...
  signaturePosition: {
    page:  1,      // Page number (1-indexed, default: last page)
    x:     20,     // X offset from left (default: 20)
    y:     20,     // Y offset from bottom (default: 20)
    width: 260     // Stamp width in points (default: 260)
  }
});

Each signature gets a visual stamp matching the CertySign platform format:

┌──────────────────────────────────────────┐
│ Digitally signed by: [email protected]│
│ Date: 2026-03-16T14:30:00.000Z            │
│ Certificate: 9EF9C8E88478FC08C6759942     │
│ Standard: PAdES Baseline B-T              │
│ Timestamp: RFC 3161 (TSA)                 │
└──────────────────────────────────────────┘

PDF Signature Structure (what gets created):

| PDF Object | Description | |------------|-------------| | /Sig dictionary | Filter: Adobe.PPKLite, SubFilter: adbe.pkcs7.detached | | /ByteRange | Byte offsets of signed regions (everything except /Contents hex) | | /Contents | DER-encoded PKCS#7 SignedData (hex, zero-padded to 8192 bytes) | | Widget annotation | Links visual stamp rectangle to the /Sig dictionary | | AcroForm | SigFlags: 3 (SignaturesExist | AppendOnly) | | Certificates | Signing cert + intermediate CA + root CA embedded in PKCS#7 |

embedInXml(xmlString, options) — Embed XMLDSig signature

Creates W3C XML Digital Signature (XMLDSig) enveloped signatures. Supports multi-signer — each signer gets a separate <ds:Signature> element.

// Single signer
const signedXml = client.embedder.embedInXml(xmlString, {
  signature:       'base64-cms-signature',
  certificate:     '-----BEGIN CERTIFICATE-----...',
  certSerialNumber: 'A1B2C3...',
  signerEmail:     '[email protected]',
  signerName:      'System',
  documentHash:    'a3f2b8c1...',
  hashAlgorithm:   'sha256',
  algorithm:       'SHA256withRSA'
});

// Multi-signer
const signedXml = client.embedder.embedInXml(xmlString, {
  signatures: [
    { signature: 'sig1', recipientEmail: '[email protected]', certSerialNumber: 'A1...' },
    { signature: 'sig2', recipientEmail: '[email protected]', certSerialNumber: 'B2...' }
  ],
  certificate: '-----BEGIN CERTIFICATE-----...',
  documentHash: 'a3f2b8c1...',
  hashAlgorithm: 'sha256',
  algorithm: 'SHA256withRSA'
});
// Returns: string — XML with <ds:Signature> elements

Each <ds:Signature> element includes signer-specific properties:

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="CertySign-Signature-0">
  <ds:SignedInfo>
    <ds:CanonicalizationMethod Algorithm="...xml-c14n11#"/>
    <ds:SignatureMethod Algorithm="...rsa-sha256"/>
    <ds:Reference URI="">
      <ds:DigestMethod Algorithm="...sha256"/>
      <ds:DigestValue>...</ds:DigestValue>
    </ds:Reference>
  </ds:SignedInfo>
  <ds:SignatureValue>...</ds:SignatureValue>
  <ds:KeyInfo>
    <ds:X509Data>
      <ds:X509Certificate>...</ds:X509Certificate>
    </ds:X509Data>
  </ds:KeyInfo>
  <ds:Object>
    <ds:SignatureProperties>
      <ds:SignatureProperty>
        <SignerEmail>[email protected]</SignerEmail>
        <CertSerial>A1...</CertSerial>
        <Standard>PAdES Baseline B-B</Standard>
      </ds:SignatureProperty>
    </ds:SignatureProperties>
  </ds:Object>
</ds:Signature>

embedInJson(jsonData, options) — Create a signed JSON envelope

Creates a signed JSON envelope with a signatures[] array. Supports multi-signer.

// Single signer
const signedJson = client.embedder.embedInJson(jsonData, {
  signature:       'base64-cms-signature',
  certificate:     '-----BEGIN CERTIFICATE-----...',
  certSerialNumber: 'A1B2C3...',
  signerEmail:     '[email protected]',
  signerName:      'API System',
  documentHash:    'a3f2b8c1...',
  hashAlgorithm:   'sha256',
  algorithm:       'SHA256withRSA'
});

// Multi-signer
const signedJson = client.embedder.embedInJson(jsonData, {
  signatures: [
    { signature: 'sig1', recipientEmail: '[email protected]', recipientName: 'Jane', certSerialNumber: 'A1...' },
    { signature: 'sig2', recipientEmail: '[email protected]', recipientName: 'James', certSerialNumber: 'B2...' }
  ],
  documentHash: 'a3f2b8c1...',
  hashAlgorithm: 'sha256',
  algorithm: 'SHA256withRSA'
});
// Returns: object

Output structure (v2 — breaking change from v1's single signature object):

{
  "data": { /* original JSON data */ },
  "signatures": [
    {
      "value": "base64-cms-sig-1",
      "algorithm": "SHA256withRSA",
      "certificate": "-----BEGIN CERTIFICATE-----...",
      "signer": {
        "email": "[email protected]",
        "name": "Jane",
        "certSerialNumber": "A1...",
        "signedAt": "2026-03-13T11:07:28.927Z"
      },
      "standard": "PAdES Baseline B-B",
      "digest": { "hash": "a3f2b8c1...", "algorithm": "sha256" }
    },
    {
      "value": "base64-cms-sig-2",
      "algorithm": "SHA256withRSA",
      "certificate": "-----BEGIN CERTIFICATE-----...",
      "signer": {
        "email": "[email protected]",
        "name": "James",
        "certSerialNumber": "B2...",
        "signedAt": "2026-03-13T11:12:45.000Z"
      },
      "standard": "PAdES Baseline B-B",
      "digest": { "hash": "a3f2b8c1...", "algorithm": "sha256" }
    }
  ],
  "metadata": {
    "provider": "CertySign Trust Services",
    "version": "2.0.0",
    "signatureCount": 2,
    "signedAt": "2026-03-13T11:12:45.000Z"
  }
}

client.dashboard — SDK Analytics

Track documents signed via the SDK, unique recipients, success/failure rates, and daily trends.

getStats(options?) — Aggregate signing statistics

const { data } = await client.dashboard.getStats({
  from: '2026-01-01',  // Optional — ISO date
  to:   '2026-03-31'   // Optional — ISO date
});

// data.documentsSignedTotal      — total documents signed across all sessions
// data.documentsSignedToday      — documents signed today
// data.documentsSignedThisMonth  — documents signed this month
// data.signingSessionsTotal      — total signing sessions created
// data.signingSessionsByStatus   — { draft, active, completed, expired, cancelled }
// data.successRate               — percentage of completed sessions
// data.uniqueRecipientsCount     — unique recipients (deduplicated by email)
// data.recipientsList            — top recipients sorted by signCount desc
// data.dailyTrend                — last 30 days: [{ date, documentsSigned }]
// data.dateRange                 — { from, to } applied filter

getRecipients(options?) — Unique recipient list with activity

const { data } = await client.dashboard.getRecipients({
  page:   1,            // Default: 1
  limit:  20,           // Default: 20
  search: 'company.co'  // Optional — filter by name or email
});

// data.recipients[] — each entry:
//   .email            — unique email address
//   .name             — recipient name
//   .totalSessions    — sessions this recipient was part of
//   .signedSessions   — sessions where they signed
//   .pendingSessions  — sessions still pending their signature
//   .declinedSessions — sessions they declined
//   .lastActivity     — last interaction timestamp
//   .firstSeen        — first time they appeared
// data.pagination     — { page, limit, total, pages }

getDocuments(options?) — Document-level signing details

const { data } = await client.dashboard.getDocuments({
  page:   1,
  limit:  20,
  status: 'completed'  // Optional — filter by session status
});

// data.documents[] — each entry:
//   .sessionId       — parent signing session ID
//   .sessionName     — session name
//   .fileName        — document file name
//   .hash            — document hash
//   .signatures[]    — per-recipient signatures with email, certSerial, signedAt
//   .signatureCount  — number of signatures on this document
//   .recipientCount  — total recipients in the session
// data.pagination    — { page, limit, total, pages }

client.certificates — X.509 Certificate Management

getActive() — Get your tenant's active signing certificate

Returns the HSM-backed certificate used for hash signing operations.

const { data } = await client.certificates.getActive();
// data.serialNumber
// data.fingerprint
// data.algorithm         — e.g. 'RSA-2048'
// data.validFrom
// data.validUntil
// data.subject           — { commonName, organization, ... }

issue(options) — Issue a per-document certificate

const result = await client.certificates.issue({
  commonName:   'DHA Claims System',        // Required — Subject CN
  organisation: 'Dept of Health Affairs',   // Required — Subject O
  country:      'KE',                       // Default: 'KE'
  state:        'Nairobi',                  // Default: 'Nairobi'
  locality:     'Nairobi',                  // Default: 'Nairobi'
  email:        '[email protected]',
  validityDays: 365,
  metadata:     { claimId: '...' }
});
// result.data.serialNumber      — store for future status checks
// result.data.fingerprint       — SHA-256 hex fingerprint
// result.data.validFrom
// result.data.validUntil
// result.data.pemCertificate    — PEM certificate (store this)
// result.data.pemChain          — Full trust chain PEM
// result.data.privateKey        — PEM private key (shown ONCE — store securely)

Issued certificates include:

  • CRL Distribution Pointhttps://pki.certysign.io/crl/intermediate.crl
  • OCSP (AIA)https://pki.certysign.io/ocsp
  • CA Issuers (AIA)https://pki.certysign.io/certs/intermediate.crt

verify(serialNumber, atTime?) — Verify certificate validity

// Current-time check
const result = await client.certificates.verify('A1B2C3...');

// Point-in-time check (was it valid when the document was signed?)
const result = await client.certificates.verify('A1B2C3...', new Date('2026-01-15T10:30:00Z'));

// result.data.valid
// result.data.chainVerified
// result.data.revocationChecked
// result.data.certStatus   'good' | 'revoked' | 'expired'

status(serialNumber) — Current revocation status

const result = await client.certificates.status('A1B2C3...');
// result.data.status           'good' | 'revoked' | 'expired' | 'unknown'
// result.data.revocationDate   ISO 8601 (if revoked)
// result.data.revocationReason  RFC 5280 reason string (if revoked)

client.pki — PKI Infrastructure

crl(format?) — Download the Certificate Revocation List

// PEM (default)
const pem = await client.pki.crl('pem');
fs.writeFileSync('/etc/pki/certysign.crl.pem', pem);

// DER binary (for nginx/caddy stapling)
const der = await client.pki.crl('der');
fs.writeFileSync('/etc/pki/certysign.crl', der);

// JSON — for application-level revocation checks
const { data } = await client.pki.crl('json');
const revokedSerials = new Set(data.crl.map(e => e.serialNumber));

CRL properties: RFC 5280 X.509 v2 CRL, signed by CertySign Intermediate CA, valid 7 days, issued every 24 hours.

ocsp(serialNumber, format?) — OCSP query

// JSON
const { data } = await client.pki.ocsp('A1B2C3...', 'json');
// data.status       'good' | 'revoked' | 'unknown'
// data.thisUpdate   ISO 8601
// data.nextUpdate   ISO 8601  ← cache response until this time
// data.responderId  CA subject DN

// DER binary (RFC 6960 BasicOCSPResponse — for OCSP stapling)
const buf = await client.pki.ocsp('A1B2C3...', 'der');

chain() — Download CA certificate chain

const pem = await client.pki.chain();
// Returns PEM bundle:  Intermediate CA cert + Root CA cert
fs.writeFileSync('/etc/ssl/certs/certysign-chain.pem', pem);

info() — CA hierarchy metadata

const { data } = await client.pki.info();
// data.rootCA.subject.commonName
// data.intermediateCA.validity.notAfter
// data.intermediateCA.crlUrl
// data.intermediateCA.ocspUrl
// data.status.initialized

client.envelopes — Envelope Management

For integrations that need full control over the envelope lifecycle.

// 1. Create envelope
const { data: { envelope } } = await client.envelopes.create({
  title:   'NHIF Claims Q1-2026',
  signers: [{ name: 'Director Finance', email: '[email protected]' }]
});

// 2. Upload documents
await client.envelopes.uploadDocuments(envelope._id, [
  { data: fs.readFileSync('./report.pdf'), filename: 'Q1-report.pdf' }
]);

// 3. Send envelope (transitions to 'sent' status)
await client.envelopes.send(envelope._id);

// 4. Sign
const signed = await client.envelopes.sign(envelope._id, {
  reason:   'NHIF Q1 approval',
  location: 'Nairobi, Kenya'
});

// 5. Download signed PDF
const pdfBuffer = await client.envelopes.getDocument(envelope._id, docId);

// 6. Audit trail
const { data } = await client.envelopes.getAuditTrail(envelope._id);
// data.auditTrail[].action, .timestamp, .eventHash, .chainHash
// data.chainIntegrity.valid

client.legacySign — Legacy Document Signing

Deprecated in v2. Uploads entire documents to CertySign for server-side signing. Use client.sign instead for hash-based signing where documents never leave your system.

// Legacy quickSign (documents are uploaded to CertySign)
const result = await client.legacySign.quickSign({
  document:    fs.readFileSync('./claim.pdf'),
  filename:    'claim.pdf',
  signerName:  'Dr. Amina Okonkwo',
  signerEmail: '[email protected]',
  reason:      'Health claim approval'
});

Legacy methods: quickSign(), batchSign(), verifyById(), verifyDocument().


Complete Examples

PDF: PAdES Sign with signCallback (Recommended)

const fs = require('fs');
const { CertySignClient } = require('@certysign/sdk');

const client = new CertySignClient({
  publicKey:   process.env.CERTYSIGN_PUBLIC_KEY,
  secretKey:   process.env.CERTYSIGN_SECRET_KEY
});

async function signPdf(filePath) {
  const pdf = fs.readFileSync(filePath);
  const cert = await client.certificates.getActive();

  const signed = await client.embedder.embedInPdf(pdf, {
    certSerialNumber: cert.data?.serialNumber,
    signerName:       'Legal Department',
    signerEmail:      '[email protected]',
    reason:           'Contract execution',
    location:         'Nairobi, Kenya',
    signCallback: async (byteRangeHash) => {
      return client.sign.signHash({
        documentHash:  byteRangeHash,
        hashAlgorithm: 'sha256',
        fileName:      filePath.split('/').pop(),
        reason:        'Contract execution'
      });
    }
  });

  const outPath = filePath.replace('.pdf', '-signed.pdf');
  fs.writeFileSync(outPath, signed);
  console.log('PAdES signed:', outPath);
}

XML: Hash → Sign → Embed XMLDSig

async function signXml(filePath) {
  const xml = fs.readFileSync(filePath, 'utf-8');
  const { hash, algorithm } = client.hasher.hash(Buffer.from(xml), 'sha256');

  const { data } = await client.sign.signHash({
    documentHash:  hash,
    hashAlgorithm: algorithm,
    fileName:      'invoice.xml'
  });

  const signedXml = client.embedder.embedInXml(xml, {
    signature:       data.signature,
    certificate:     data.certificate,       // PEM string
    certSerialNumber: data.certSerialNumber,
    documentHash:    hash,
    hashAlgorithm:   algorithm,
    algorithm:       data.algorithm
  });

  fs.writeFileSync(filePath.replace('.xml', '-signed.xml'), signedXml);
}

JSON: Hash → Sign → Envelope

async function signJson(inputData) {
  const json = JSON.stringify(inputData);
  const { hash, algorithm } = client.hasher.hash(Buffer.from(json), 'sha256');

  const { data } = await client.sign.signHash({
    documentHash:  hash,
    hashAlgorithm: algorithm,
    fileName:      'payload.json'
  });

  return client.embedder.embedInJson(inputData, {
    signature:       data.signature,
    certificate:     data.certificate,       // PEM string
    certSerialNumber: data.certSerialNumber,
    documentHash:    hash,
    hashAlgorithm:   algorithm,
    algorithm:       data.algorithm
  });
}

Batch Signing (50 invoices)

const invoiceDir = './invoices/pending/';
const files = fs.readdirSync(invoiceDir).filter(f => f.endsWith('.pdf'));

// Hash all locally
const documents = files.map(f => ({
  document: fs.readFileSync(`${invoiceDir}${f}`),
  fileName: f
}));

// One API call for all 50
const { data } = await client.sign.batchHashAndSign({
  documents,
  signerName: 'NHIF Finance System',
  reason:     'Monthly provider reimbursement'
});

// Embed PAdES signatures locally using signCallback for each
for (let i = 0; i < data.results.length; i++) {
  const signed = await client.embedder.embedInPdf(documents[i].document, {
    certSerialNumber: data.certSerialNumber,
    signerName:       'NHIF Finance System',
    signerEmail:      '[email protected]',
    reason:           'Monthly provider reimbursement',
    signCallback: async (byteRangeHash) => {
      return client.sign.signHash({
        documentHash:  byteRangeHash,
        hashAlgorithm: 'sha256',
        fileName:      files[i]
      });
    }
  });
  fs.writeFileSync(`./invoices/signed/${files[i]}`, signed);
}

Multi-Recipient Signing Session → Embed

const fs = require('fs');
const { CertySignClient } = require('@certysign/sdk');

const client = new CertySignClient({
  publicKey:   process.env.CERTYSIGN_PUBLIC_KEY,
  secretKey:   process.env.CERTYSIGN_SECRET_KEY
});

async function multiRecipientSign() {
  const pdf = fs.readFileSync('./board-resolution.pdf');
  const { hash, algorithm } = client.hasher.hash(pdf, 'sha256');

  // 1. Create signing session with multiple recipients
  const { data: session } = await client.sessions.create({
    name: 'Board Resolution Q1 2026',
    documents: [{ fileName: 'board-resolution.pdf', hash, hashAlgorithm: algorithm }],
    recipients: [
      { name: 'Jane Mwangi', email: '[email protected]', role: 'signer' },
      { name: 'James Otieno', email: '[email protected]', role: 'signer' }
    ],
    signingOrder: 'sequential', // or 'parallel'
    expiresIn: '7d'
  });

  // 2. Each recipient signs after OTP verification
  //    (Normally triggered from the recipient's signing link)
  //    After all recipients sign, session status becomes 'completed'

  // 3. Retrieve completed session with all signatures
  const { data: completed } = await client.sessions.get(session.sessionId);

  // 4. Embed all signatures into the PDF
  const doc = completed.documents[0];
  const signedPdf = await client.embedder.embedInPdf(pdf, {
    signatures:    doc.signatures,  // Array of per-recipient signatures
    documentHash:  doc.hash,
    hashAlgorithm: doc.hashAlgorithm
  });

  fs.writeFileSync('./board-resolution-signed.pdf', signedPdf);
  // PDF now has stacked visual stamps for each signer
}

Dashboard: Track SDK Signing Activity

async function showDashboard() {
  // Overall stats
  const { data: stats } = await client.dashboard.getStats({
    from: '2026-01-01',
    to:   '2026-03-31'
  });
  console.log(`Total documents signed: ${stats.documentsSignedTotal}`);
  console.log(`Success rate: ${stats.successRate}%`);
  console.log(`Unique recipients: ${stats.uniqueRecipientsCount}`);

  // Daily trend chart data
  stats.dailyTrend.forEach(d => {
    console.log(`  ${d.date}: ${d.documentsSigned} documents`);
  });

  // Paginated recipient list
  const { data: recipients } = await client.dashboard.getRecipients({
    page: 1, limit: 10, search: 'company.co'
  });
  recipients.recipients.forEach(r => {
    console.log(`${r.email}: ${r.signedSessions} signed, ${r.pendingSessions} pending`);
  });

  // Document-level details
  const { data: docs } = await client.dashboard.getDocuments({
    status: 'completed', page: 1, limit: 10
  });
  docs.documents.forEach(d => {
    console.log(`${d.fileName}: ${d.signatureCount} signatures`);
  });
}

Error Handling

All SDK methods throw CertySignError on failure:

const { CertySignClient, CertySignError } = require('@certysign/sdk');

try {
  await client.sign.hashAndSign({ document: pdfBuffer });
} catch (err) {
  if (err instanceof CertySignError) {
    console.error(err.message);       // Human-readable message
    console.error(err.statusCode);    // HTTP status (0 for network errors)
    console.error(err.code);          // Machine-readable code, e.g. 'INVALID_API_KEY'
    console.error(err.details);       // Full API error body
  }
}

Common error codes:

| Code | Status | Meaning | |------|--------|---------| | INVALID_API_KEY | 401 | Key not found or secret mismatch | | API_KEY_EXPIRED | 401 | Key has passed its expiresAt date | | INSUFFICIENT_SDK_PERMISSIONS | 403 | Key lacks required permission | | IP_NOT_ALLOWED | 403 | Request IP not in IP allowlist | | RATE_LIMIT_EXCEEDED | 429 | Requests per minute exceeded | | NO_ACTIVE_CERTIFICATE | 404 | Tenant has no active signing certificate | | CERTIFICATE_REVOKED | 400 | Active certificate has been revoked | | INVALID_HASH | 400 | Hash format is invalid (must be hex) | | INVALID_OTP | 400 | OTP code is incorrect | | OTP_EXPIRED | 400 | OTP has expired (10 minute window) | | OTP_MAX_ATTEMPTS | 400 | Too many failed OTP attempts (max 5) | | SIGNING_TOKEN_EXPIRED | 400 | Signing token has expired (30 minute window) | | SESSION_EXPIRED | 400 | Signing session has expired | | SIGNING_ORDER_VIOLATION | 400 | Previous recipient hasn't signed yet (sequential mode) | | NETWORK_ERROR | 0 | Could not reach the API |

The SDK automatically retries 429, 502, 503, 504 responses up to 3 times with exponential back-off.


Rate Limiting

Each API key has a configurable rate limit (requests per minute, set in the CertySign portal). When the limit is exceeded:

  • HTTP 429 is returned
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers are set
  • SDK retries automatically (up to 3 times) if the issue is transient
  • After retries are exhausted, CertySignError with code RATE_LIMIT_EXCEEDED is thrown

For batch workloads, use batchHashAndSign() or batchSignHashes() to sign up to 50 documents in a single API call.


Migration from v1

What's New in v2.2.0

| Feature | Description | |---------|-------------| | PAdES Baseline B-T | Every PDF signature now includes an RFC 3161 timestamp from CertySign's HSM-backed TSA | | Automatic TSA URL | TSA URL is resolved from your environment — no manual tsaUrl parameter needed | | HSM-backed TSA key | Timestamps are signed with a dedicated HSM key (RSA-3072, FIPS 140-2 Level 3) | | OTP email delivery | OTP emails for signing sessions are now fully operational | | Audit blockchain anchoring | Signing session events (SIGNING_SESSION_RECIPIENT_SIGNED, SIGNING_OTP_VERIFIED) are now archived to WORM storage and included in daily Merkle tree anchoring |

What's New in v2.1.0

| Feature | Description | |---------|-------------| | PAdES cryptographic embedding | embedInPdf() now creates proper PDF /Sig dictionaries with PKCS#7/CMS that Adobe Reader and Foxit validate | | signCallback option | Pass an async callback to embedInPdf() — the embedder computes ByteRange hash, your callback signs it via HSM | | PKCS#7 SignedData | Full CMS ASN.1 structure: signing cert + chain embedded, SHA-256 digest algorithm, detached content | | ByteRange signing | Signature covers the actual PDF ByteRange regions, not just the original document hash | | node-forge dependency | Added for ASN.1/PKCS#7 construction |

Constructor Change (v2.2.0)

The tsaUrl parameter is now optional. TSA is resolved automatically:

// v2.1.0 — manual TSA URL required for timestamps
const client = new CertySignClient({
  publicKey, secretKey,
  tsaUrl: 'http://localhost:5015'
});

// v2.2.0 — TSA is automatic
const client = new CertySignClient({
  publicKey, secretKey,
  environment: 'production'   // TSA URL auto-resolved
});

Response Shape Change (v2.1.0)

The signing API response has a flat structure (not nested):

// v2.0.0 (incorrect docs):
result.data.certificate.pem          // ❌ was never an object
result.data.certificate.serialNumber  // ❌
result.data.certificate.chain         // ❌

// v2.1.0 (correct — always was this shape):
result.data.certificate         // ✅ PEM string directly
result.data.certSerialNumber    // ✅ hex serial string
result.data.chain               // ✅ PEM chain string
result.data.certFingerprint     // ✅ SHA-256 hex fingerprint

Breaking Changes in v2.0.0

| v1 | v2 | Notes | |----|-----|-------| | client.sign.quickSign() | client.sign.hashAndSign() | Documents no longer uploaded. Hash locally, sign remotely. | | client.sign.batchSign() | client.sign.batchHashAndSign() | Same principle — only hashes cross the network. | | client.sign (SigningResource) | client.legacySign | Old file-upload signing moved to legacySign | | — | client.sign (HashSigningResource) | New hash-based signing is now client.sign | | — | client.sessions | New multi-recipient signing sessions with OTP | | — | client.hasher | New local document hashing utility | | — | client.embedder | New local signature embedding (PDF/XML/JSON) | | — | client.dashboard | New SDK analytics (stats, recipients, documents) | | — | client.certificates.getActive() | New active cert retrieval | | embedInJsonsignature: {} | embedInJsonsignatures: [] | JSON output changed from single object to array |

Migration Steps

  1. Replace quickSign with hashAndSign + embedInPdf:

    // v1 — document uploaded to CertySign
    await client.sign.quickSign({ document: pdf, filename: 'doc.pdf', signerName: '...' });
    
    // v2 — document stays local
    const result = await client.sign.hashAndSign({ document: pdf, fileName: 'doc.pdf' });
    const signed = await client.embedder.embedInPdf(pdf, { signature: result.data.signature, ... });
  2. Replace batchSign with batchHashAndSign:

    // v1
    await client.sign.batchSign({ documents: [...] });
    
    // v2
    const result = await client.sign.batchHashAndSign({ documents: [...] });
    // Then embed each signature locally
  3. Use client.legacySign if you need the old behavior temporarily:

    // Still works — but deprecated
    await client.legacySign.quickSign({ ... });

PKI Trust Chain

CertySign Root CA (C=KE, RSA-4096, 20yr)
  └── CertySign Intermediate CA (C=KE, RSA-4096, 10yr, pathLen=0)
        ├── Per-document X.509 certs  (issued via /sdk/v1/certificates/issue)
        └── Tenant signing certs      (HSM-backed, used for hash signing)

CA bundle: https://pki.certysign.io/certs/chain.pem
CRL: https://pki.certysign.io/crl/intermediate.crl
OCSP: https://pki.certysign.io/ocsp


Security Best Practices

  • Documents never leave your system — only cryptographic hashes are sent to CertySign
  • Rotate keys every 90 days: use the CertySign portal → API Keys → Rotate
  • IP allowlist every production key to your deployment's egress IPs
  • Use staging environment for development and CI pipelines (separate key pair)
  • Store the private key from certificates.issue() response in a HSM or secrets manager — CertySign does not retain it
  • Cache CRL / OCSP responses up to nextUpdate to reduce latency in HIE systems
  • Verify hashes after embedding — re-hash the original document to confirm integrity
  • Secure OTP delivery — OTPs are single-use, expire in 10 minutes, and lock after 5 failed attempts. OTP codes are stored as SHA-256 hashes and verified with constant-time comparison
  • Signing tokens — 30-minute expiry, cryptographically random (32 bytes), single-use, stored as SHA-256 hashes
  • Timestamps — every signature includes an RFC 3161 timestamp from an HSM-backed TSA, proving when the document was signed
  • Audit immutability — signing events are archived to WORM object storage and anchored via daily Merkle trees

Timestamp Authority (TSA)

All PDF signatures include an RFC 3161 timestamp that cryptographically proves when the document was signed. The timestamp is issued by CertySign's HSM-backed Timestamp Authority and embedded as an unsigned attribute in the PKCS#7 SignedData.

How It Works

| Step | What happens | |------|--------------| | 1 | The PDF ByteRange hash is signed by the tenant's HSM key | | 2 | The signature bytes are hashed (SHA-256) | | 3 | The hash is sent to the TSA (POST /v1/tsa/json) | | 4 | The TSA signs the hash with its dedicated HSM key and returns an RFC 3161 TimeStampToken | | 5 | The TimeStampToken is added as an unsigned attribute (OID 1.2.840.113549.1.9.16.2.14) in the PKCS#7 |

TSA URL Resolution

The TSA URL is resolved automatically from your environment:

// No tsaUrl needed — resolved from environment
const client = new CertySignClient({
  publicKey: '...',
  secretKey: '...',
  environment: 'production'   // → https://tsa.certysign.io
});

console.log(client.tsaUrl);   // https://tsa.certysign.io

| Environment | TSA URL | |-------------|----------| | production | https://tsa.certysign.io | | staging | https://tsa-staging.certysign.io | | development | http://localhost:5015 | | test | http://localhost:5015 |

Override with tsaUrl if you run a custom TSA:

const client = new CertySignClient({
  publicKey: '...', secretKey: '...',
  tsaUrl: 'https://custom-tsa.example.com'
});

PAdES Compliance Levels

| Standard | Timestamp | When | |----------|-----------|------| | PAdES Baseline B-T | RFC 3161 embedded | TSA URL available (default) | | PAdES Baseline B-B | None | TSA URL is null (manually disabled) |

Since v2.2.0, all environments have TSA configured by default, so every signature is PAdES Baseline B-T unless you explicitly pass tsaUrl: null.

Verifying Timestamps

The timestamp token is embedded in the PKCS#7 SignedData as an unsigned attribute. PDF validators (Adobe Reader, Foxit) display:

Signature is timestamped
The signature includes an embedded timestamp.
Timestamp time: 2026-03-17T09:05:29.000Z

To verify programmatically, parse the PKCS#7 ASN.1 and check for the unsigned attribute with OID 1.2.840.113549.1.9.16.2.14 (id-smime-aa-timeStampToken).


Support

  • Docs: https://docs.certysign.io/sdk
  • Issues: https://github.com/certysign/sdk-node/issues
  • Email: [email protected]