@certysign/sdk 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +398 -213
- package/package.json +2 -1
- package/src/index.js +20 -2
- package/src/lib/SignatureEmbedder.js +520 -119
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Official Node.js SDK for **CertySign Trust Services** — hash-based digital sig
|
|
|
26
26
|
- [Complete Examples](#complete-examples)
|
|
27
27
|
- [Error Handling](#error-handling)
|
|
28
28
|
- [Rate Limiting](#rate-limiting)
|
|
29
|
+
- [Timestamp Authority (TSA)](#timestamp-authority-tsa)
|
|
29
30
|
- [Migration from v1](#migration-from-v1)
|
|
30
31
|
- [PKI Trust Chain](#pki-trust-chain)
|
|
31
32
|
- [Security Best Practices](#security-best-practices)
|
|
@@ -35,65 +36,26 @@ Official Node.js SDK for **CertySign Trust Services** — hash-based digital sig
|
|
|
35
36
|
|
|
36
37
|
## Installation
|
|
37
38
|
|
|
38
|
-
### Local / internal use (current)
|
|
39
|
-
|
|
40
|
-
The SDK is not yet published to the public npm registry. Use one of the methods below.
|
|
41
|
-
|
|
42
|
-
**Option A — file path dependency** (recommended for services in this monorepo):
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
# From any service directory, e.g. a DHA integration project:
|
|
46
|
-
npm install ../../sdk
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
This adds the following to the consuming project's `package.json`:
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
"dependencies": {
|
|
53
|
-
"@certysign/sdk": "file:../../sdk"
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Option B — `npm link`** (for local development outside the monorepo):
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
# In the sdk/ directory — register it globally once:
|
|
61
|
-
cd sdk
|
|
62
|
-
npm link
|
|
63
|
-
|
|
64
|
-
# In any other project:
|
|
65
|
-
npm link @certysign/sdk
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Option C — install directly from the folder path**:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm install /absolute/path/to/Certy_Sign/sdk
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Publishing to npm (when ready)
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
# Log in to npm:
|
|
78
|
-
npm login
|
|
79
|
-
|
|
80
|
-
# From the sdk/ directory:
|
|
81
|
-
npm publish --access public
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
After publishing, standard installation will work:
|
|
85
|
-
|
|
86
39
|
```bash
|
|
87
40
|
npm install @certysign/sdk
|
|
88
41
|
```
|
|
89
42
|
|
|
90
43
|
Node.js >= 18 required.
|
|
91
44
|
|
|
45
|
+
**Peer dependencies** (installed automatically):
|
|
46
|
+
- `pdf-lib` — PDF manipulation for visual stamps and /Sig dictionaries
|
|
47
|
+
- `node-forge` — PKCS#7/CMS ASN.1 construction for cryptographic embedding
|
|
48
|
+
|
|
92
49
|
---
|
|
93
50
|
|
|
94
51
|
## Quick Start
|
|
95
52
|
|
|
96
|
-
###
|
|
53
|
+
### PAdES Signing — Cryptographically Valid PDF Signatures (Recommended)
|
|
54
|
+
|
|
55
|
+
This is the **recommended** approach for v2.2.0+. It produces **PAdES Baseline B-T**
|
|
56
|
+
PDF signatures with embedded RFC 3161 timestamps, recognised as valid by
|
|
57
|
+
Adobe Reader, Foxit Reader, and other PDF validators. The TSA URL is resolved
|
|
58
|
+
automatically from your environment — no configuration needed.
|
|
97
59
|
|
|
98
60
|
```js
|
|
99
61
|
const { CertySignClient } = require('@certysign/sdk');
|
|
@@ -105,28 +67,68 @@ const client = new CertySignClient({
|
|
|
105
67
|
environment: 'production' // 'staging' | 'development'
|
|
106
68
|
});
|
|
107
69
|
|
|
108
|
-
// 1. Hash locally — document never leaves your system
|
|
109
70
|
const pdfBuffer = fs.readFileSync('./claim.pdf');
|
|
110
71
|
|
|
111
|
-
//
|
|
72
|
+
// Get the active certificate for the visual stamp
|
|
73
|
+
const cert = await client.certificates.getActive();
|
|
74
|
+
|
|
75
|
+
// One call does everything:
|
|
76
|
+
// 1. Prepares PDF with visual stamp + /Sig placeholder
|
|
77
|
+
// 2. Computes SHA-256 hash of the ByteRange regions
|
|
78
|
+
// 3. Calls your signCallback to sign the hash via HSM
|
|
79
|
+
// 4. Builds PKCS#7 SignedData and patches into /Contents
|
|
80
|
+
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
81
|
+
certSerialNumber: cert.data?.serialNumber,
|
|
82
|
+
signerEmail: 'dr.okonkwo@dha.go.ke',
|
|
83
|
+
signerName: 'Dr. Amina Okonkwo',
|
|
84
|
+
reason: 'Health claim approval — DHA Kenya',
|
|
85
|
+
location: 'Nairobi, Kenya',
|
|
86
|
+
signCallback: async (byteRangeHash) => {
|
|
87
|
+
// The embedder computed the ByteRange hash — now sign it via HSM
|
|
88
|
+
return client.sign.signHash({
|
|
89
|
+
documentHash: byteRangeHash,
|
|
90
|
+
hashAlgorithm: 'sha256',
|
|
91
|
+
fileName: 'claim.pdf',
|
|
92
|
+
reason: 'Health claim approval'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync('./claim-signed.pdf', signedPdf);
|
|
98
|
+
// Open in Adobe Reader → Signature Panel shows:
|
|
99
|
+
// ✓ Valid digital signature
|
|
100
|
+
// ✓ Timestamp: RFC 3161 (proves when it was signed)
|
|
101
|
+
// ✓ Standard: PAdES Baseline B-T
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Simple Signing — Hash → Sign → Embed (Legacy approach)
|
|
105
|
+
|
|
106
|
+
This approach signs the document hash first, then embeds the pre-computed signature.
|
|
107
|
+
The PDF will have visual stamps and PKCS#7 structure, but the signature covers the
|
|
108
|
+
original document hash rather than the ByteRange, so PDF readers may show a warning.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const pdfBuffer = fs.readFileSync('./claim.pdf');
|
|
112
|
+
|
|
113
|
+
// 1. Hash locally & sign remotely
|
|
112
114
|
const result = await client.sign.hashAndSign({
|
|
113
115
|
document: pdfBuffer,
|
|
114
116
|
fileName: 'claim.pdf',
|
|
115
117
|
signerName: 'Dr. Amina Okonkwo',
|
|
116
|
-
reason: 'Health claim approval
|
|
117
|
-
location: 'Nairobi, Kenya'
|
|
118
|
+
reason: 'Health claim approval'
|
|
118
119
|
});
|
|
119
120
|
|
|
120
|
-
//
|
|
121
|
+
// 2. Embed signature into PDF
|
|
121
122
|
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
122
|
-
signature:
|
|
123
|
-
certificate:
|
|
124
|
-
certSerialNumber: result.data.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
signature: result.data.signature,
|
|
124
|
+
certificate: result.data.certificate, // PEM string
|
|
125
|
+
certSerialNumber: result.data.certSerialNumber,
|
|
126
|
+
chain: result.data.chain, // PEM chain string
|
|
127
|
+
signerName: 'Dr. Amina Okonkwo',
|
|
128
|
+
reason: 'Health claim approval',
|
|
129
|
+
documentHash: result.data.documentHash,
|
|
130
|
+
hashAlgorithm: result.data.hashAlgorithm,
|
|
131
|
+
algorithm: result.data.algorithm
|
|
130
132
|
});
|
|
131
133
|
|
|
132
134
|
fs.writeFileSync('./claim-signed.pdf', signedPdf);
|
|
@@ -172,46 +174,53 @@ const signed = await client.sessions.recipientSign(
|
|
|
172
174
|
|
|
173
175
|
## Architecture
|
|
174
176
|
|
|
177
|
+
### PAdES Flow (Recommended — `signCallback`)
|
|
178
|
+
|
|
175
179
|
```
|
|
176
|
-
|
|
177
|
-
│ SUBSCRIBER'S INFRASTRUCTURE
|
|
178
|
-
│
|
|
179
|
-
│
|
|
180
|
-
│ │
|
|
181
|
-
│ │
|
|
182
|
-
│ │
|
|
183
|
-
│
|
|
184
|
-
│
|
|
185
|
-
│
|
|
186
|
-
│
|
|
187
|
-
│
|
|
188
|
-
│
|
|
189
|
-
│
|
|
190
|
-
│
|
|
191
|
-
│
|
|
192
|
-
│
|
|
193
|
-
│
|
|
194
|
-
│
|
|
195
|
-
│
|
|
196
|
-
│
|
|
197
|
-
│
|
|
198
|
-
│
|
|
199
|
-
│
|
|
200
|
-
│
|
|
201
|
-
│
|
|
202
|
-
│
|
|
203
|
-
│
|
|
204
|
-
│
|
|
205
|
-
│ │
|
|
206
|
-
│
|
|
207
|
-
│ │
|
|
208
|
-
│
|
|
209
|
-
│
|
|
210
|
-
|
|
180
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
181
|
+
│ SUBSCRIBER'S INFRASTRUCTURE │
|
|
182
|
+
│ │
|
|
183
|
+
│ ┌───────────┐ │
|
|
184
|
+
│ │ PDF │ │
|
|
185
|
+
│ │ Document │─────────────┐ │
|
|
186
|
+
│ └───────────┘ ▼ │
|
|
187
|
+
│ ┌──────────────────────────────┐ │
|
|
188
|
+
│ │ embedInPdf(pdf, { │ │
|
|
189
|
+
│ │ signCallback: async (h) => │ 1. Prepare PDF │
|
|
190
|
+
│ │ client.sign.signHash() │ + visual stamp │
|
|
191
|
+
│ │ }) │ + /Sig dict │
|
|
192
|
+
│ └──────────┬───────────────────┘ │
|
|
193
|
+
│ │ 2. SHA-256(ByteRange) │
|
|
194
|
+
│ ▼ │
|
|
195
|
+
│ ┌───────────────────────────────────┐ │
|
|
196
|
+
│ │ CertySign API │ │
|
|
197
|
+
│ │ POST /sdk/v1/sign/hash │ │
|
|
198
|
+
│ │ ┌─────────────────────────────┐ │ │
|
|
199
|
+
│ │ │ HSM (Google Cloud KMS) │ │ │
|
|
200
|
+
│ │ │ Signs ByteRange hash │ │ │
|
|
201
|
+
│ │ └─────────────────────────────┘ │ │
|
|
202
|
+
│ └───────────────┬───────────────────┘ │
|
|
203
|
+
│ │ RSA signature + cert PEM + chain │
|
|
204
|
+
│ ▼ │
|
|
205
|
+
│ ┌──────────────────────────────┐ │
|
|
206
|
+
│ │ 3. Build PKCS#7 SignedData │ │
|
|
207
|
+
│ │ 4. Patch into /Contents │ │
|
|
208
|
+
│ │ 5. Patch /ByteRange │ │
|
|
209
|
+
│ └──────────────┬───────────────┘ │
|
|
210
|
+
│ ▼ │
|
|
211
|
+
│ ┌──────────────────────────────┐ │
|
|
212
|
+
│ │ PAdES-Signed PDF │ │
|
|
213
|
+
│ │ ✓ Adobe Reader validates │ │
|
|
214
|
+
│ │ ✓ /Sig + /ByteRange + PKCS#7 │ │
|
|
215
|
+
│ │ ✓ RFC 3161 timestamp (TSA) │ │
|
|
216
|
+
│ │ ✓ Stays on YOUR system │ │
|
|
217
|
+
│ └──────────────────────────────┘ │
|
|
218
|
+
│ │
|
|
219
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
211
220
|
```
|
|
212
221
|
|
|
213
|
-
**What crosses the network:** Only the
|
|
214
|
-
**What stays local:** The original document, the
|
|
222
|
+
**What crosses the network:** Only the SHA-256 hash of the PDF ByteRange (64 hex characters).
|
|
223
|
+
**What stays local:** The original document, the signing preparation, the PKCS#7 construction, and the final signed PDF.
|
|
215
224
|
|
|
216
225
|
---
|
|
217
226
|
|
|
@@ -232,11 +241,30 @@ The secret key is shown **once** at creation. Store it in a secrets manager (Vau
|
|
|
232
241
|
|
|
233
242
|
## Environments
|
|
234
243
|
|
|
235
|
-
| Environment | Base URL |
|
|
236
|
-
|
|
237
|
-
| `production` | `https://
|
|
238
|
-
| `staging` | `https://
|
|
239
|
-
| `development` | `http://localhost:8000` |
|
|
244
|
+
| Environment | Base URL | TSA URL |
|
|
245
|
+
|-------------|----------|----------|
|
|
246
|
+
| `production` | `https://core.certysign.io` | `https://tsa.certysign.io` |
|
|
247
|
+
| `staging` | `https://service.certysign.io` | `https://tsa-staging.certysign.io` |
|
|
248
|
+
| `development` | `http://localhost:8000` | `http://localhost:5015` |
|
|
249
|
+
|
|
250
|
+
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.
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
// TSA is automatic — just set your environment
|
|
254
|
+
const client = new CertySignClient({
|
|
255
|
+
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
256
|
+
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
257
|
+
environment: 'production' // TSA → https://tsa.certysign.io (automatic)
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Or override TSA URL explicitly
|
|
261
|
+
const client = new CertySignClient({
|
|
262
|
+
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
263
|
+
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
264
|
+
environment: 'production',
|
|
265
|
+
tsaUrl: 'https://custom-tsa.example.com' // Custom TSA endpoint
|
|
266
|
+
});
|
|
267
|
+
```
|
|
240
268
|
|
|
241
269
|
---
|
|
242
270
|
|
|
@@ -259,14 +287,15 @@ const result = await client.sign.hashAndSign({
|
|
|
259
287
|
location: 'Nairobi, Kenya', // Optional
|
|
260
288
|
metadata: { claimId: '...' } // Optional
|
|
261
289
|
});
|
|
262
|
-
// result.data.signature — base64-encoded
|
|
290
|
+
// result.data.signature — base64-encoded raw RSA signature
|
|
263
291
|
// result.data.documentHash — hex hash of the document
|
|
264
292
|
// result.data.hashAlgorithm — 'sha256' | 'sha384' | 'sha512'
|
|
265
293
|
// result.data.algorithm — 'SHA256withRSA' etc.
|
|
266
|
-
// result.data.certificate
|
|
267
|
-
// result.data.
|
|
268
|
-
// result.data.
|
|
269
|
-
// result.data.
|
|
294
|
+
// result.data.certificate — signer certificate PEM string
|
|
295
|
+
// result.data.chain — full trust chain PEM string
|
|
296
|
+
// result.data.certSerialNumber — certificate serial (hex)
|
|
297
|
+
// result.data.certFingerprint — SHA-256 fingerprint (hex)
|
|
298
|
+
// result.data.timestamp — ISO 8601 timestamp
|
|
270
299
|
```
|
|
271
300
|
|
|
272
301
|
#### `signHash(options)` — Sign a pre-computed hash
|
|
@@ -300,7 +329,9 @@ const result = await client.sign.batchHashAndSign({
|
|
|
300
329
|
// result.data.results[] — array of signed results
|
|
301
330
|
// result.data.results[].documentHash
|
|
302
331
|
// result.data.results[].signature
|
|
303
|
-
// result.data.certificate
|
|
332
|
+
// result.data.certificate — shared certificate PEM for the batch
|
|
333
|
+
// result.data.certSerialNumber — certificate serial (hex)
|
|
334
|
+
// result.data.chain — chain PEM
|
|
304
335
|
```
|
|
305
336
|
|
|
306
337
|
#### `batchSignHashes(options)` — Sign multiple pre-computed hashes
|
|
@@ -535,75 +566,99 @@ const results = await client.hasher.hashFiles([
|
|
|
535
566
|
|
|
536
567
|
### `client.embedder` — Local Signature Embedding
|
|
537
568
|
|
|
538
|
-
Embeds CMS/PKCS#7 signatures into documents **locally** on your machine.
|
|
569
|
+
Embeds CMS/PKCS#7 signatures into documents **locally** on your machine. PDF embedding creates cryptographically valid PAdES signatures recognised by Adobe Reader and Foxit.
|
|
570
|
+
|
|
571
|
+
#### `embedInPdf(pdfBuffer, options)` — Embed PAdES signature into PDF
|
|
572
|
+
|
|
573
|
+
Creates a visual signature stamp and a proper PDF `/Sig` dictionary with PKCS#7/CMS
|
|
574
|
+
cryptographic embedding. The resulting PDF passes validation in Adobe Reader, Foxit
|
|
575
|
+
Reader, and other PDF signature validators.
|
|
539
576
|
|
|
540
|
-
|
|
577
|
+
**Recommended: `signCallback` approach (PAdES Baseline B-T)**
|
|
541
578
|
|
|
542
|
-
|
|
579
|
+
The `signCallback` approach produces cryptographically valid PAdES B-T signatures
|
|
580
|
+
with embedded RFC 3161 timestamps:
|
|
543
581
|
|
|
544
|
-
|
|
582
|
+
1. The embedder prepares the PDF with visual stamp and an empty `/Sig` placeholder
|
|
583
|
+
2. It computes the SHA-256 hash of the PDF's ByteRange regions
|
|
584
|
+
3. Your `signCallback` sends that hash to CertySign's HSM for signing
|
|
585
|
+
4. The embedder fetches an RFC 3161 timestamp from the TSA (automatic)
|
|
586
|
+
5. The embedder builds a PKCS#7 SignedData structure with the timestamp as an unsigned attribute and patches it into `/Contents`
|
|
545
587
|
|
|
546
588
|
```js
|
|
547
589
|
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
590
|
+
signerEmail: 'ndolimathews@gmail.com', // Shown on visual stamp
|
|
591
|
+
signerName: 'Mathews Ndoli', // Fallback if no email
|
|
592
|
+
certSerialNumber: '9EF9C8E88478FC08C6759942274EB8A2', // Shown on visual stamp
|
|
593
|
+
reason: 'Document approval', // Shown on stamp + /Sig dict
|
|
594
|
+
location: 'Nairobi, Kenya', // Optional
|
|
595
|
+
signCallback: async (byteRangeHash) => {
|
|
596
|
+
// byteRangeHash is a hex-encoded SHA-256 of the ByteRange regions
|
|
597
|
+
return client.sign.signHash({
|
|
598
|
+
documentHash: byteRangeHash,
|
|
599
|
+
hashAlgorithm: 'sha256',
|
|
600
|
+
fileName: 'document.pdf'
|
|
601
|
+
});
|
|
602
|
+
// Must return: { data: { signature, certificate, chain, certSerialNumber } }
|
|
603
|
+
}
|
|
557
604
|
});
|
|
558
|
-
// Returns: Buffer — signed PDF
|
|
605
|
+
// Returns: Buffer — PAdES-signed PDF
|
|
559
606
|
fs.writeFileSync('./signed.pdf', signedPdf);
|
|
560
607
|
```
|
|
561
608
|
|
|
562
|
-
**
|
|
609
|
+
**Alternative: Pre-computed signature (legacy)**
|
|
610
|
+
|
|
611
|
+
You can pass a pre-computed `signature` instead of `signCallback`. The signature
|
|
612
|
+
will be embedded in a PKCS#7 structure, but since it was computed over the original
|
|
613
|
+
document hash (not the ByteRange), PDF readers may not fully validate it.
|
|
563
614
|
|
|
564
615
|
```js
|
|
565
616
|
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
617
|
+
signature: result.data.signature, // Base64 raw RSA signature
|
|
618
|
+
certificate: result.data.certificate, // PEM string
|
|
619
|
+
chain: result.data.chain, // PEM chain string
|
|
620
|
+
certSerialNumber: result.data.certSerialNumber,
|
|
621
|
+
signerEmail: 'ndolimathews@gmail.com',
|
|
622
|
+
reason: 'Approval'
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Signature Position:**
|
|
627
|
+
|
|
628
|
+
```js
|
|
629
|
+
const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
630
|
+
// ... signCallback or signature ...
|
|
631
|
+
signaturePosition: {
|
|
632
|
+
page: 1, // Page number (1-indexed, default: last page)
|
|
633
|
+
x: 20, // X offset from left (default: 20)
|
|
634
|
+
y: 20, // Y offset from bottom (default: 20)
|
|
635
|
+
width: 260 // Stamp width in points (default: 260)
|
|
636
|
+
}
|
|
586
637
|
});
|
|
587
638
|
```
|
|
588
639
|
|
|
589
|
-
Each
|
|
640
|
+
Each signature gets a visual stamp matching the CertySign platform format:
|
|
590
641
|
|
|
591
642
|
```
|
|
592
|
-
|
|
593
|
-
│ Digitally signed by:
|
|
594
|
-
│ Date: 2026-03-
|
|
595
|
-
│ Certificate:
|
|
596
|
-
│ Standard: PAdES Baseline B-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
│ Digitally signed by: ceo@company.co.ke│
|
|
600
|
-
│ Date: 2026-03-13T11:12:45.000Z │
|
|
601
|
-
│ Certificate: F6E5D4C3B2A1... │
|
|
602
|
-
│ Standard: PAdES Baseline B-B │
|
|
603
|
-
└──────────────────────────────────────┘
|
|
643
|
+
┌──────────────────────────────────────────┐
|
|
644
|
+
│ Digitally signed by: ndolimathews@gmail.com│
|
|
645
|
+
│ Date: 2026-03-16T14:30:00.000Z │
|
|
646
|
+
│ Certificate: 9EF9C8E88478FC08C6759942 │
|
|
647
|
+
│ Standard: PAdES Baseline B-T │
|
|
648
|
+
│ Timestamp: RFC 3161 (TSA) │
|
|
649
|
+
└──────────────────────────────────────────┘
|
|
604
650
|
```
|
|
605
651
|
|
|
606
|
-
|
|
652
|
+
**PDF Signature Structure (what gets created):**
|
|
653
|
+
|
|
654
|
+
| PDF Object | Description |
|
|
655
|
+
|------------|-------------|
|
|
656
|
+
| `/Sig` dictionary | `Filter: Adobe.PPKLite`, `SubFilter: adbe.pkcs7.detached` |
|
|
657
|
+
| `/ByteRange` | Byte offsets of signed regions (everything except `/Contents` hex) |
|
|
658
|
+
| `/Contents` | DER-encoded PKCS#7 SignedData (hex, zero-padded to 8192 bytes) |
|
|
659
|
+
| Widget annotation | Links visual stamp rectangle to the `/Sig` dictionary |
|
|
660
|
+
| AcroForm | `SigFlags: 3` (SignaturesExist \| AppendOnly) |
|
|
661
|
+
| Certificates | Signing cert + intermediate CA + root CA embedded in PKCS#7 |
|
|
607
662
|
|
|
608
663
|
#### `embedInXml(xmlString, options)` — Embed XMLDSig signature
|
|
609
664
|
|
|
@@ -815,14 +870,12 @@ Returns the HSM-backed certificate used for hash signing operations.
|
|
|
815
870
|
|
|
816
871
|
```js
|
|
817
872
|
const { data } = await client.certificates.getActive();
|
|
818
|
-
// data.
|
|
819
|
-
// data.
|
|
820
|
-
// data.
|
|
821
|
-
// data.
|
|
822
|
-
// data.
|
|
823
|
-
// data.
|
|
824
|
-
// data.certificate.subject — { commonName, organization, ... }
|
|
825
|
-
// data.certificate.trustSettings — signing capabilities
|
|
873
|
+
// data.serialNumber
|
|
874
|
+
// data.fingerprint
|
|
875
|
+
// data.algorithm — e.g. 'RSA-2048'
|
|
876
|
+
// data.validFrom
|
|
877
|
+
// data.validUntil
|
|
878
|
+
// data.subject — { commonName, organization, ... }
|
|
826
879
|
```
|
|
827
880
|
|
|
828
881
|
#### `issue(options)` — Issue a per-document certificate
|
|
@@ -990,7 +1043,7 @@ Legacy methods: `quickSign()`, `batchSign()`, `verifyById()`, `verifyDocument()`
|
|
|
990
1043
|
|
|
991
1044
|
## Complete Examples
|
|
992
1045
|
|
|
993
|
-
### PDF:
|
|
1046
|
+
### PDF: PAdES Sign with signCallback (Recommended)
|
|
994
1047
|
|
|
995
1048
|
```js
|
|
996
1049
|
const fs = require('fs');
|
|
@@ -1003,30 +1056,27 @@ const client = new CertySignClient({
|
|
|
1003
1056
|
|
|
1004
1057
|
async function signPdf(filePath) {
|
|
1005
1058
|
const pdf = fs.readFileSync(filePath);
|
|
1059
|
+
const cert = await client.certificates.getActive();
|
|
1006
1060
|
|
|
1007
|
-
// Hash & sign — document stays local
|
|
1008
|
-
const { data } = await client.sign.hashAndSign({
|
|
1009
|
-
document: pdf,
|
|
1010
|
-
fileName: 'contract.pdf',
|
|
1011
|
-
signerName: 'Legal Department',
|
|
1012
|
-
reason: 'Contract execution'
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
// Embed signature into PDF locally
|
|
1016
1061
|
const signed = await client.embedder.embedInPdf(pdf, {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1062
|
+
certSerialNumber: cert.data?.serialNumber,
|
|
1063
|
+
signerName: 'Legal Department',
|
|
1064
|
+
signerEmail: 'legal@company.co.ke',
|
|
1065
|
+
reason: 'Contract execution',
|
|
1066
|
+
location: 'Nairobi, Kenya',
|
|
1067
|
+
signCallback: async (byteRangeHash) => {
|
|
1068
|
+
return client.sign.signHash({
|
|
1069
|
+
documentHash: byteRangeHash,
|
|
1070
|
+
hashAlgorithm: 'sha256',
|
|
1071
|
+
fileName: filePath.split('/').pop(),
|
|
1072
|
+
reason: 'Contract execution'
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1026
1075
|
});
|
|
1027
1076
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1077
|
+
const outPath = filePath.replace('.pdf', '-signed.pdf');
|
|
1078
|
+
fs.writeFileSync(outPath, signed);
|
|
1079
|
+
console.log('PAdES signed:', outPath);
|
|
1030
1080
|
}
|
|
1031
1081
|
```
|
|
1032
1082
|
|
|
@@ -1044,12 +1094,12 @@ async function signXml(filePath) {
|
|
|
1044
1094
|
});
|
|
1045
1095
|
|
|
1046
1096
|
const signedXml = client.embedder.embedInXml(xml, {
|
|
1047
|
-
signature:
|
|
1048
|
-
certificate:
|
|
1049
|
-
certSerialNumber: data.
|
|
1050
|
-
documentHash:
|
|
1051
|
-
hashAlgorithm:
|
|
1052
|
-
algorithm:
|
|
1097
|
+
signature: data.signature,
|
|
1098
|
+
certificate: data.certificate, // PEM string
|
|
1099
|
+
certSerialNumber: data.certSerialNumber,
|
|
1100
|
+
documentHash: hash,
|
|
1101
|
+
hashAlgorithm: algorithm,
|
|
1102
|
+
algorithm: data.algorithm
|
|
1053
1103
|
});
|
|
1054
1104
|
|
|
1055
1105
|
fs.writeFileSync(filePath.replace('.xml', '-signed.xml'), signedXml);
|
|
@@ -1059,23 +1109,23 @@ async function signXml(filePath) {
|
|
|
1059
1109
|
### JSON: Hash → Sign → Envelope
|
|
1060
1110
|
|
|
1061
1111
|
```js
|
|
1062
|
-
async function signJson(
|
|
1063
|
-
const json = JSON.stringify(
|
|
1112
|
+
async function signJson(inputData) {
|
|
1113
|
+
const json = JSON.stringify(inputData);
|
|
1064
1114
|
const { hash, algorithm } = client.hasher.hash(Buffer.from(json), 'sha256');
|
|
1065
1115
|
|
|
1066
|
-
const { data
|
|
1116
|
+
const { data } = await client.sign.signHash({
|
|
1067
1117
|
documentHash: hash,
|
|
1068
1118
|
hashAlgorithm: algorithm,
|
|
1069
1119
|
fileName: 'payload.json'
|
|
1070
1120
|
});
|
|
1071
1121
|
|
|
1072
|
-
return client.embedder.embedInJson(
|
|
1073
|
-
signature:
|
|
1074
|
-
certificate:
|
|
1075
|
-
certSerialNumber:
|
|
1076
|
-
documentHash:
|
|
1077
|
-
hashAlgorithm:
|
|
1078
|
-
algorithm:
|
|
1122
|
+
return client.embedder.embedInJson(inputData, {
|
|
1123
|
+
signature: data.signature,
|
|
1124
|
+
certificate: data.certificate, // PEM string
|
|
1125
|
+
certSerialNumber: data.certSerialNumber,
|
|
1126
|
+
documentHash: hash,
|
|
1127
|
+
hashAlgorithm: algorithm,
|
|
1128
|
+
algorithm: data.algorithm
|
|
1079
1129
|
});
|
|
1080
1130
|
}
|
|
1081
1131
|
```
|
|
@@ -1099,15 +1149,20 @@ const { data } = await client.sign.batchHashAndSign({
|
|
|
1099
1149
|
reason: 'Monthly provider reimbursement'
|
|
1100
1150
|
});
|
|
1101
1151
|
|
|
1102
|
-
// Embed signatures locally
|
|
1152
|
+
// Embed PAdES signatures locally using signCallback for each
|
|
1103
1153
|
for (let i = 0; i < data.results.length; i++) {
|
|
1104
1154
|
const signed = await client.embedder.embedInPdf(documents[i].document, {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1155
|
+
certSerialNumber: data.certSerialNumber,
|
|
1156
|
+
signerName: 'NHIF Finance System',
|
|
1157
|
+
signerEmail: 'finance@nhif.or.ke',
|
|
1158
|
+
reason: 'Monthly provider reimbursement',
|
|
1159
|
+
signCallback: async (byteRangeHash) => {
|
|
1160
|
+
return client.sign.signHash({
|
|
1161
|
+
documentHash: byteRangeHash,
|
|
1162
|
+
hashAlgorithm: 'sha256',
|
|
1163
|
+
fileName: files[i]
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1111
1166
|
});
|
|
1112
1167
|
fs.writeFileSync(`./invoices/signed/${files[i]}`, signed);
|
|
1113
1168
|
}
|
|
@@ -1256,6 +1311,61 @@ For batch workloads, use `batchHashAndSign()` or `batchSignHashes()` to sign up
|
|
|
1256
1311
|
|
|
1257
1312
|
## Migration from v1
|
|
1258
1313
|
|
|
1314
|
+
### What's New in v2.2.0
|
|
1315
|
+
|
|
1316
|
+
| Feature | Description |
|
|
1317
|
+
|---------|-------------|
|
|
1318
|
+
| **PAdES Baseline B-T** | Every PDF signature now includes an RFC 3161 timestamp from CertySign's HSM-backed TSA |
|
|
1319
|
+
| **Automatic TSA URL** | TSA URL is resolved from your environment — no manual `tsaUrl` parameter needed |
|
|
1320
|
+
| **HSM-backed TSA key** | Timestamps are signed with a dedicated HSM key (RSA-3072, FIPS 140-2 Level 3) |
|
|
1321
|
+
| **OTP email delivery** | OTP emails for signing sessions are now fully operational |
|
|
1322
|
+
| **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 |
|
|
1323
|
+
|
|
1324
|
+
### What's New in v2.1.0
|
|
1325
|
+
|
|
1326
|
+
| Feature | Description |
|
|
1327
|
+
|---------|-------------|
|
|
1328
|
+
| **PAdES cryptographic embedding** | `embedInPdf()` now creates proper PDF `/Sig` dictionaries with PKCS#7/CMS that Adobe Reader and Foxit validate |
|
|
1329
|
+
| **`signCallback` option** | Pass an async callback to `embedInPdf()` — the embedder computes ByteRange hash, your callback signs it via HSM |
|
|
1330
|
+
| **PKCS#7 SignedData** | Full CMS ASN.1 structure: signing cert + chain embedded, SHA-256 digest algorithm, detached content |
|
|
1331
|
+
| **ByteRange signing** | Signature covers the actual PDF ByteRange regions, not just the original document hash |
|
|
1332
|
+
| **`node-forge` dependency** | Added for ASN.1/PKCS#7 construction |
|
|
1333
|
+
|
|
1334
|
+
#### Constructor Change (v2.2.0)
|
|
1335
|
+
|
|
1336
|
+
The `tsaUrl` parameter is now optional. TSA is resolved automatically:
|
|
1337
|
+
|
|
1338
|
+
```js
|
|
1339
|
+
// v2.1.0 — manual TSA URL required for timestamps
|
|
1340
|
+
const client = new CertySignClient({
|
|
1341
|
+
publicKey, secretKey,
|
|
1342
|
+
tsaUrl: 'http://localhost:5015'
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
// v2.2.0 — TSA is automatic
|
|
1346
|
+
const client = new CertySignClient({
|
|
1347
|
+
publicKey, secretKey,
|
|
1348
|
+
environment: 'production' // TSA URL auto-resolved
|
|
1349
|
+
});
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
#### Response Shape Change (v2.1.0)
|
|
1353
|
+
|
|
1354
|
+
The signing API response has a flat structure (not nested):
|
|
1355
|
+
|
|
1356
|
+
```js
|
|
1357
|
+
// v2.0.0 (incorrect docs):
|
|
1358
|
+
result.data.certificate.pem // ❌ was never an object
|
|
1359
|
+
result.data.certificate.serialNumber // ❌
|
|
1360
|
+
result.data.certificate.chain // ❌
|
|
1361
|
+
|
|
1362
|
+
// v2.1.0 (correct — always was this shape):
|
|
1363
|
+
result.data.certificate // ✅ PEM string directly
|
|
1364
|
+
result.data.certSerialNumber // ✅ hex serial string
|
|
1365
|
+
result.data.chain // ✅ PEM chain string
|
|
1366
|
+
result.data.certFingerprint // ✅ SHA-256 hex fingerprint
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1259
1369
|
### Breaking Changes in v2.0.0
|
|
1260
1370
|
|
|
1261
1371
|
| v1 | v2 | Notes |
|
|
@@ -1325,8 +1435,83 @@ OCSP: `https://pki.certysign.io/ocsp`
|
|
|
1325
1435
|
- **Store the private key** from `certificates.issue()` response in a HSM or secrets manager — CertySign does not retain it
|
|
1326
1436
|
- **Cache CRL / OCSP responses** up to `nextUpdate` to reduce latency in HIE systems
|
|
1327
1437
|
- **Verify hashes** after embedding — re-hash the original document to confirm integrity
|
|
1328
|
-
- **Secure OTP delivery** — OTPs are single-use, expire in 10 minutes, and lock after 5 failed attempts
|
|
1329
|
-
- **Signing tokens** — 30-minute expiry, cryptographically random, single-use
|
|
1438
|
+
- **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
|
|
1439
|
+
- **Signing tokens** — 30-minute expiry, cryptographically random (32 bytes), single-use, stored as SHA-256 hashes
|
|
1440
|
+
- **Timestamps** — every signature includes an RFC 3161 timestamp from an HSM-backed TSA, proving when the document was signed
|
|
1441
|
+
- **Audit immutability** — signing events are archived to WORM object storage and anchored via daily Merkle trees
|
|
1442
|
+
|
|
1443
|
+
---
|
|
1444
|
+
|
|
1445
|
+
## Timestamp Authority (TSA)
|
|
1446
|
+
|
|
1447
|
+
All PDF signatures include an **RFC 3161 timestamp** that cryptographically proves
|
|
1448
|
+
when the document was signed. The timestamp is issued by CertySign's HSM-backed
|
|
1449
|
+
Timestamp Authority and embedded as an unsigned attribute in the PKCS#7 SignedData.
|
|
1450
|
+
|
|
1451
|
+
### How It Works
|
|
1452
|
+
|
|
1453
|
+
| Step | What happens |
|
|
1454
|
+
|------|--------------|
|
|
1455
|
+
| 1 | The PDF ByteRange hash is signed by the tenant's HSM key |
|
|
1456
|
+
| 2 | The signature bytes are hashed (SHA-256) |
|
|
1457
|
+
| 3 | The hash is sent to the TSA (`POST /v1/tsa/json`) |
|
|
1458
|
+
| 4 | The TSA signs the hash with its dedicated HSM key and returns an RFC 3161 TimeStampToken |
|
|
1459
|
+
| 5 | The TimeStampToken is added as an unsigned attribute (OID `1.2.840.113549.1.9.16.2.14`) in the PKCS#7 |
|
|
1460
|
+
|
|
1461
|
+
### TSA URL Resolution
|
|
1462
|
+
|
|
1463
|
+
The TSA URL is resolved **automatically** from your environment:
|
|
1464
|
+
|
|
1465
|
+
```js
|
|
1466
|
+
// No tsaUrl needed — resolved from environment
|
|
1467
|
+
const client = new CertySignClient({
|
|
1468
|
+
publicKey: '...',
|
|
1469
|
+
secretKey: '...',
|
|
1470
|
+
environment: 'production' // → https://tsa.certysign.io
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
console.log(client.tsaUrl); // https://tsa.certysign.io
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
| Environment | TSA URL |
|
|
1477
|
+
|-------------|----------|
|
|
1478
|
+
| `production` | `https://tsa.certysign.io` |
|
|
1479
|
+
| `staging` | `https://tsa-staging.certysign.io` |
|
|
1480
|
+
| `development` | `http://localhost:5015` |
|
|
1481
|
+
| `test` | `http://localhost:5015` |
|
|
1482
|
+
|
|
1483
|
+
Override with `tsaUrl` if you run a custom TSA:
|
|
1484
|
+
|
|
1485
|
+
```js
|
|
1486
|
+
const client = new CertySignClient({
|
|
1487
|
+
publicKey: '...', secretKey: '...',
|
|
1488
|
+
tsaUrl: 'https://custom-tsa.example.com'
|
|
1489
|
+
});
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### PAdES Compliance Levels
|
|
1493
|
+
|
|
1494
|
+
| Standard | Timestamp | When |
|
|
1495
|
+
|----------|-----------|------|
|
|
1496
|
+
| **PAdES Baseline B-T** | RFC 3161 embedded | TSA URL available (default) |
|
|
1497
|
+
| **PAdES Baseline B-B** | None | TSA URL is `null` (manually disabled) |
|
|
1498
|
+
|
|
1499
|
+
Since v2.2.0, all environments have TSA configured by default, so every signature
|
|
1500
|
+
is **PAdES Baseline B-T** unless you explicitly pass `tsaUrl: null`.
|
|
1501
|
+
|
|
1502
|
+
### Verifying Timestamps
|
|
1503
|
+
|
|
1504
|
+
The timestamp token is embedded in the PKCS#7 SignedData as an unsigned attribute.
|
|
1505
|
+
PDF validators (Adobe Reader, Foxit) display:
|
|
1506
|
+
|
|
1507
|
+
```
|
|
1508
|
+
Signature is timestamped
|
|
1509
|
+
The signature includes an embedded timestamp.
|
|
1510
|
+
Timestamp time: 2026-03-17T09:05:29.000Z
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
To verify programmatically, parse the PKCS#7 ASN.1 and check for the unsigned
|
|
1514
|
+
attribute with OID `1.2.840.113549.1.9.16.2.14` (id-smime-aa-timeStampToken).
|
|
1330
1515
|
|
|
1331
1516
|
---
|
|
1332
1517
|
|