@certysign/sdk 1.0.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 -0
- package/examples/certificate-flow.js +198 -0
- package/examples/dha-integration.js +208 -0
- package/examples/nhif-batch-sign.js +216 -0
- package/package.json +45 -0
- package/src/index.js +161 -0
- package/src/lib/CertificateResource.js +146 -0
- package/src/lib/EnvelopeResource.js +249 -0
- package/src/lib/HttpClient.js +198 -0
- package/src/lib/PkiResource.js +170 -0
- package/src/lib/SigningResource.js +192 -0
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# @certysign/sdk
|
|
2
|
+
|
|
3
|
+
Official Node.js SDK for **CertySign Trust Services** — digital document signing, X.509 certificate issuance, and PKI operations built for East Africa.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Local / internal use (current)
|
|
8
|
+
|
|
9
|
+
The SDK is not yet published to the public npm registry. Use one of the methods below.
|
|
10
|
+
|
|
11
|
+
**Option A — file path dependency** (recommended for services in this monorepo):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# From any service directory, e.g. a DHA integration project:
|
|
15
|
+
npm install ../../sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This adds the following to the consuming project's `package.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@certysign/sdk": "file:../../sdk"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Option B — `npm link`** (for local development outside the monorepo):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# In the sdk/ directory — register it globally once:
|
|
30
|
+
cd sdk
|
|
31
|
+
npm link
|
|
32
|
+
|
|
33
|
+
# In any other project:
|
|
34
|
+
npm link @certysign/sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Option C — install directly from the folder path**:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install /absolute/path/to/Certy_Sign/sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Publishing to npm (when ready)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Log in to npm:
|
|
47
|
+
npm login
|
|
48
|
+
|
|
49
|
+
# From the sdk/ directory:
|
|
50
|
+
npm publish --access public
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
After publishing, standard installation will work:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @certysign/sdk
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Node.js >= 18 required.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const { CertySignClient } = require('@certysign/sdk');
|
|
65
|
+
|
|
66
|
+
const client = new CertySignClient({
|
|
67
|
+
publicKey: process.env.CERTYSIGN_PUBLIC_KEY, // cs_pk_...
|
|
68
|
+
secretKey: process.env.CERTYSIGN_SECRET_KEY, // cs_sk_...
|
|
69
|
+
environment: 'production' // 'staging' | 'development'
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Sign a document
|
|
73
|
+
const result = await client.sign.quickSign({
|
|
74
|
+
document: require('fs').readFileSync('./claim.pdf'),
|
|
75
|
+
filename: 'claim.pdf',
|
|
76
|
+
signerName: 'Dr. Amina Okonkwo',
|
|
77
|
+
signerEmail: 'amina.okonkwo@dha.go.ke',
|
|
78
|
+
reason: 'Health claim approval — DHA Kenya'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log(result.data.envelopeId);
|
|
82
|
+
console.log(result.data.certificate.serialNumber);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Authentication
|
|
88
|
+
|
|
89
|
+
API keys are managed in the CertySign portal under **Settings → API Keys**.
|
|
90
|
+
|
|
91
|
+
Each key pair consists of:
|
|
92
|
+
|
|
93
|
+
| Value | Header | Format |
|
|
94
|
+
|-------|--------|--------|
|
|
95
|
+
| Public key | `X-API-Key-Id` | `cs_pk_<48 hex chars>` |
|
|
96
|
+
| Secret key | `X-API-Key-Secret` | `cs_sk_<64 hex chars>` |
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Environments
|
|
103
|
+
|
|
104
|
+
| Environment | Base URL |
|
|
105
|
+
|-------------|----------|
|
|
106
|
+
| `production` | `https://api.certysign.com` |
|
|
107
|
+
| `staging` | `https://api-staging.certysign.com` |
|
|
108
|
+
| `development` | `http://localhost:8000` |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## API Resources
|
|
113
|
+
|
|
114
|
+
### `client.sign` — Document Signing
|
|
115
|
+
|
|
116
|
+
#### `quickSign(options)` — Sign a PDF in one call
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const result = await client.sign.quickSign({
|
|
120
|
+
document: Buffer, // Required — PDF buffer or ReadStream
|
|
121
|
+
filename: 'document.pdf', // Optional — default: 'document.pdf'
|
|
122
|
+
signerName: 'Full Name', // Required
|
|
123
|
+
signerEmail: 'email@example.com', // Optional
|
|
124
|
+
reason: 'Approval', // Optional
|
|
125
|
+
location: 'Nairobi, Kenya', // Optional
|
|
126
|
+
metadata: { claimId: '...' } // Optional — stored with envelope
|
|
127
|
+
});
|
|
128
|
+
// result.data.envelopeId
|
|
129
|
+
// result.data.certificate.serialNumber
|
|
130
|
+
// result.data.signedDocumentUrl
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `batchSign(options)` — Sign multiple PDFs at once
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const result = await client.sign.batchSign({
|
|
137
|
+
documents: [
|
|
138
|
+
{ data: fs.readFileSync('./invoice-1.pdf'), filename: 'invoice-1.pdf' },
|
|
139
|
+
{ data: fs.readFileSync('./invoice-2.pdf'), filename: 'invoice-2.pdf' }
|
|
140
|
+
],
|
|
141
|
+
signerName: 'NHIF Finance System',
|
|
142
|
+
signerEmail: 'invoicing@nhif.or.ke',
|
|
143
|
+
reason: 'Batch provider reimbursement'
|
|
144
|
+
});
|
|
145
|
+
// result.data.results[].envelopeId
|
|
146
|
+
// result.data.results[].success
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### `verifyById(envelopeId)` — Verify a signed envelope
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
const result = await client.sign.verifyById('env_abc123');
|
|
153
|
+
// result.data.valid true/false
|
|
154
|
+
// result.data.signerName
|
|
155
|
+
// result.data.signedAt
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### `verifyDocument(document, filename)` — Verify by uploading
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
const result = await client.sign.verifyDocument(fs.readFileSync('./signed.pdf'));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### `client.certificates` — X.509 Certificate Management
|
|
167
|
+
|
|
168
|
+
#### `issue(options)` — Issue a per-document certificate
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const result = await client.certificates.issue({
|
|
172
|
+
commonName: 'DHA Claims System', // Required — Subject CN
|
|
173
|
+
organisation: 'Dept of Health Affairs', // Required — Subject O
|
|
174
|
+
country: 'KE', // Default: 'KE'
|
|
175
|
+
state: 'Nairobi', // Default: 'Nairobi'
|
|
176
|
+
locality: 'Nairobi', // Default: 'Nairobi'
|
|
177
|
+
email: 'pki@dha.go.ke',
|
|
178
|
+
validityDays: 365,
|
|
179
|
+
metadata: { claimId: '...' }
|
|
180
|
+
});
|
|
181
|
+
// result.data.serialNumber — store for future status checks
|
|
182
|
+
// result.data.fingerprint — SHA-256 hex fingerprint
|
|
183
|
+
// result.data.validFrom
|
|
184
|
+
// result.data.validUntil
|
|
185
|
+
// result.data.pemCertificate — PEM certificate (store this)
|
|
186
|
+
// result.data.pemChain — Full trust chain PEM
|
|
187
|
+
// result.data.privateKey — PEM private key (shown ONCE — store securely)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Issued certificates include:
|
|
191
|
+
- **CRL Distribution Point** → `https://pki.certysign.com/crl/intermediate.crl`
|
|
192
|
+
- **OCSP (AIA)** → `https://pki.certysign.com/ocsp`
|
|
193
|
+
- **CA Issuers (AIA)** → `https://pki.certysign.com/certs/intermediate.crt`
|
|
194
|
+
|
|
195
|
+
#### `verify(serialNumber, atTime?)` — Verify certificate validity
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
// Current-time check
|
|
199
|
+
const result = await client.certificates.verify('A1B2C3...');
|
|
200
|
+
|
|
201
|
+
// Point-in-time check (was it valid when the document was signed?)
|
|
202
|
+
const result = await client.certificates.verify('A1B2C3...', new Date('2026-01-15T10:30:00Z'));
|
|
203
|
+
|
|
204
|
+
// result.data.valid
|
|
205
|
+
// result.data.chainVerified
|
|
206
|
+
// result.data.revocationChecked
|
|
207
|
+
// result.data.certStatus 'good' | 'revoked' | 'expired'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `status(serialNumber)` — Current revocation status
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
const result = await client.certificates.status('A1B2C3...');
|
|
214
|
+
// result.data.status 'good' | 'revoked' | 'expired' | 'unknown'
|
|
215
|
+
// result.data.revocationDate ISO 8601 (if revoked)
|
|
216
|
+
// result.data.revocationReason RFC 5280 reason string (if revoked)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### `client.pki` — PKI Infrastructure
|
|
222
|
+
|
|
223
|
+
#### `crl(format?)` — Download the Certificate Revocation List
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
// PEM (default)
|
|
227
|
+
const pem = await client.pki.crl('pem');
|
|
228
|
+
fs.writeFileSync('/etc/pki/certysign.crl.pem', pem);
|
|
229
|
+
|
|
230
|
+
// DER binary (for nginx/caddy stapling)
|
|
231
|
+
const der = await client.pki.crl('der');
|
|
232
|
+
fs.writeFileSync('/etc/pki/certysign.crl', der);
|
|
233
|
+
|
|
234
|
+
// JSON — for application-level revocation checks
|
|
235
|
+
const { data } = await client.pki.crl('json');
|
|
236
|
+
const revokedSerials = new Set(data.crl.map(e => e.serialNumber));
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
CRL properties: RFC 5280 X.509 v2 CRL, signed by CertySign Intermediate CA, valid 7 days, issued every 24 hours.
|
|
240
|
+
|
|
241
|
+
#### `ocsp(serialNumber, format?)` — OCSP query
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
// JSON
|
|
245
|
+
const { data } = await client.pki.ocsp('A1B2C3...', 'json');
|
|
246
|
+
// data.status 'good' | 'revoked' | 'unknown'
|
|
247
|
+
// data.thisUpdate ISO 8601
|
|
248
|
+
// data.nextUpdate ISO 8601 ← cache response until this time
|
|
249
|
+
// data.responderId CA subject DN
|
|
250
|
+
|
|
251
|
+
// DER binary (RFC 6960 BasicOCSPResponse — for OCSP stapling)
|
|
252
|
+
const buf = await client.pki.ocsp('A1B2C3...', 'der');
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `chain()` — Download CA certificate chain
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
const pem = await client.pki.chain();
|
|
259
|
+
// Returns PEM bundle: Intermediate CA cert + Root CA cert
|
|
260
|
+
fs.writeFileSync('/etc/ssl/certs/certysign-chain.pem', pem);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### `info()` — CA hierarchy metadata
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
const { data } = await client.pki.info();
|
|
267
|
+
// data.rootCA.subject.commonName
|
|
268
|
+
// data.intermediateCA.validity.notAfter
|
|
269
|
+
// data.intermediateCA.crlUrl
|
|
270
|
+
// data.intermediateCA.ocspUrl
|
|
271
|
+
// data.status.initialized
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### `client.envelopes` — Envelope Management
|
|
277
|
+
|
|
278
|
+
For integrations that need full control over the envelope lifecycle (instead of `quickSign`).
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
// 1. Create envelope
|
|
282
|
+
const { data: { envelope } } = await client.envelopes.create({
|
|
283
|
+
title: 'NHIF Claims Q1-2026',
|
|
284
|
+
signers: [{ name: 'Director Finance', email: 'df@nhif.or.ke' }]
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// 2. Upload documents
|
|
288
|
+
await client.envelopes.uploadDocuments(envelope._id, [
|
|
289
|
+
{ data: fs.readFileSync('./report.pdf'), filename: 'Q1-report.pdf' }
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
// 3. Send envelope (transitions to 'sent' status)
|
|
293
|
+
await client.envelopes.send(envelope._id);
|
|
294
|
+
|
|
295
|
+
// 4. Sign
|
|
296
|
+
const signed = await client.envelopes.sign(envelope._id, {
|
|
297
|
+
reason: 'NHIF Q1 approval',
|
|
298
|
+
location: 'Nairobi, Kenya'
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// 5. Download signed PDF
|
|
302
|
+
const pdfBuffer = await client.envelopes.getDocument(envelope._id, docId);
|
|
303
|
+
|
|
304
|
+
// 6. Audit trail
|
|
305
|
+
const { data } = await client.envelopes.getAuditTrail(envelope._id);
|
|
306
|
+
// data.auditTrail[].action, .timestamp, .eventHash, .chainHash
|
|
307
|
+
// data.chainIntegrity.valid
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Error Handling
|
|
313
|
+
|
|
314
|
+
All SDK methods throw `CertySignError` on failure:
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
const { CertySignClient, CertySignError } = require('@certysign/sdk');
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await client.sign.quickSign({ ... });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (err instanceof CertySignError) {
|
|
323
|
+
console.error(err.message); // Human-readable message
|
|
324
|
+
console.error(err.statusCode); // HTTP status (0 for network errors)
|
|
325
|
+
console.error(err.code); // Machine-readable code, e.g. 'INVALID_API_KEY'
|
|
326
|
+
console.error(err.details); // Full API error body
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Common error codes:
|
|
332
|
+
|
|
333
|
+
| Code | Status | Meaning |
|
|
334
|
+
|------|--------|---------|
|
|
335
|
+
| `INVALID_API_KEY` | 401 | Key not found or secret mismatch |
|
|
336
|
+
| `API_KEY_EXPIRED` | 401 | Key has passed its `expiresAt` date |
|
|
337
|
+
| `INSUFFICIENT_SDK_PERMISSIONS` | 403 | Key lacks required permission |
|
|
338
|
+
| `IP_NOT_ALLOWED` | 403 | Request IP not in IP allowlist |
|
|
339
|
+
| `RATE_LIMIT_EXCEEDED` | 429 | Requests per minute exceeded |
|
|
340
|
+
| `NETWORK_ERROR` | 0 | Could not reach the API |
|
|
341
|
+
|
|
342
|
+
The SDK automatically retries `429`, `502`, `503`, `504` responses up to 3 times with exponential back-off.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Rate Limiting
|
|
347
|
+
|
|
348
|
+
Each API key has a configurable rate limit (requests per minute, set in the CertySign portal). When the limit is exceeded:
|
|
349
|
+
|
|
350
|
+
- HTTP `429` is returned
|
|
351
|
+
- `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers are set
|
|
352
|
+
- SDK retries automatically (up to 3 times) if the issue is transient
|
|
353
|
+
- After retries are exhausted, `CertySignError` with code `RATE_LIMIT_EXCEEDED` is thrown
|
|
354
|
+
|
|
355
|
+
For batch workloads, use `batchSign()` to sign multiple documents in a single API call rather than calling `quickSign()` in a loop.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Examples
|
|
360
|
+
|
|
361
|
+
| File | Description |
|
|
362
|
+
|------|-------------|
|
|
363
|
+
| [examples/dha-integration.js](examples/dha-integration.js) | DHA Kenya claims quick-sign + certificate issuance |
|
|
364
|
+
| [examples/nhif-batch-sign.js](examples/nhif-batch-sign.js) | NHIF nightly invoice batch signing |
|
|
365
|
+
| [examples/certificate-flow.js](examples/certificate-flow.js) | Full X.509 lifecycle: issue, verify, CRL, OCSP, chain |
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## PKI Trust Chain
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
CertySign Root CA (C=KE, RSA-4096, 20yr)
|
|
373
|
+
└── CertySign Intermediate CA (C=KE, RSA-4096, 10yr, pathLen=0)
|
|
374
|
+
├── Per-document X.509 certs (issued via /sdk/v1/certificates/issue)
|
|
375
|
+
└── Tenant signing certs (managed in CertySign portal)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
CA bundle: `https://pki.certysign.com/certs/chain.pem`
|
|
379
|
+
CRL: `https://pki.certysign.com/crl/intermediate.crl`
|
|
380
|
+
OCSP: `https://pki.certysign.com/ocsp`
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Security Best Practices
|
|
385
|
+
|
|
386
|
+
- **Rotate keys** every 90 days: use the CertySign portal → API Keys → Rotate
|
|
387
|
+
- **IP allowlist** every production key to your deployment's egress IPs
|
|
388
|
+
- **Use `staging` environment** for development and CI pipelines (separate key pair)
|
|
389
|
+
- **Store the private key** from `certificates.issue()` response in a HSM or secrets manager — CertySign does not retain it
|
|
390
|
+
- **Cache CRL / OCSP responses** up to `nextUpdate` to reduce latency in HIE systems
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Support
|
|
395
|
+
|
|
396
|
+
- Docs: https://docs.certysign.com/sdk
|
|
397
|
+
- Issues: https://github.com/certysign/sdk-node/issues
|
|
398
|
+
- Email: sdk@certysign.com
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full PKI Certificate Lifecycle Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the complete X.509 certificate workflow:
|
|
5
|
+
* 1. Issue a per-document certificate
|
|
6
|
+
* 2. Check current status (should be 'good')
|
|
7
|
+
* 3. Verify the certificate (chain, revocation, validity)
|
|
8
|
+
* 4. Download CA chain and save it locally
|
|
9
|
+
* 5. Perform a point-in-time verification
|
|
10
|
+
* 6. Fetch and cache the CRL locally
|
|
11
|
+
* 7. Check a serial number directly against the CRL JSON
|
|
12
|
+
* 8. Perform an OCSP query
|
|
13
|
+
*
|
|
14
|
+
* This pattern is used by HIE systems that need to:
|
|
15
|
+
* - Verify document signing certificates before granting access
|
|
16
|
+
* - Cache CRL/OCSP responses to meet response time SLAs
|
|
17
|
+
*
|
|
18
|
+
* Prerequisites:
|
|
19
|
+
* npm install @certysign/sdk
|
|
20
|
+
* export CERTYSIGN_PUBLIC_KEY="cs_pk_..."
|
|
21
|
+
* export CERTYSIGN_SECRET_KEY="cs_sk_..."
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { CertySignClient, CertySignError } = require('@certysign/sdk');
|
|
29
|
+
|
|
30
|
+
const client = new CertySignClient({
|
|
31
|
+
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
32
|
+
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
33
|
+
environment: 'production'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function runCertificateFlow() {
|
|
37
|
+
console.log('\n[PKI] CertySign Certificate Lifecycle Demo');
|
|
38
|
+
console.log('─'.repeat(60));
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// 1. Issue a certificate
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
43
|
+
console.log('\n[PKI] 1. Issuing X.509 certificate...');
|
|
44
|
+
|
|
45
|
+
const issueResult = await client.certificates.issue({
|
|
46
|
+
commonName: 'HIE Integration Node — KNH',
|
|
47
|
+
organisation: 'Kenyatta National Hospital',
|
|
48
|
+
country: 'KE',
|
|
49
|
+
state: 'Nairobi',
|
|
50
|
+
locality: 'Nairobi',
|
|
51
|
+
email: 'pki@knh.or.ke',
|
|
52
|
+
validityDays: 365,
|
|
53
|
+
metadata: { system: 'HIE-NODE-KNH-PROD', issuedAt: new Date().toISOString() }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const cert = issueResult.data;
|
|
57
|
+
console.log(` serialNumber : ${cert.serialNumber}`);
|
|
58
|
+
console.log(` fingerprint : ${cert.fingerprint}`);
|
|
59
|
+
console.log(` validFrom : ${cert.validFrom}`);
|
|
60
|
+
console.log(` validUntil : ${cert.validUntil}`);
|
|
61
|
+
console.log(` Subject CN : ${cert.subject?.cn}`);
|
|
62
|
+
|
|
63
|
+
// Store private key securely (shown ONCE — CertySign does not retain it)
|
|
64
|
+
const keyPath = path.join(__dirname, 'hie-node-knh.key.pem');
|
|
65
|
+
fs.writeFileSync(keyPath, cert.privateKey, { mode: 0o600 });
|
|
66
|
+
console.log(` privateKey → ${keyPath} (mode 600)`);
|
|
67
|
+
|
|
68
|
+
const certPath = path.join(__dirname, 'hie-node-knh.crt.pem');
|
|
69
|
+
fs.writeFileSync(certPath, cert.pemCertificate);
|
|
70
|
+
console.log(` certificate → ${certPath}`);
|
|
71
|
+
|
|
72
|
+
const { serialNumber } = cert;
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// 2. Real-time status check (should be 'good')
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
77
|
+
console.log('\n[PKI] 2. Certificate status check...');
|
|
78
|
+
const statusResult = await client.certificates.status(serialNumber);
|
|
79
|
+
const status = statusResult.data;
|
|
80
|
+
console.log(` status : ${status.status}`);
|
|
81
|
+
if (status.status === 'revoked') {
|
|
82
|
+
console.log(` revocationDate : ${status.revocationDate}`);
|
|
83
|
+
console.log(` reason : ${status.revocationReason}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// 3. Full certificate verification (chain + revocation)
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
89
|
+
console.log('\n[PKI] 3. Full certificate verification...');
|
|
90
|
+
const verifyResult = await client.certificates.verify(serialNumber);
|
|
91
|
+
const verification = verifyResult.data;
|
|
92
|
+
console.log(` valid : ${verification.valid}`);
|
|
93
|
+
console.log(` chainVerified : ${verification.chainVerified}`);
|
|
94
|
+
console.log(` revocationChecked : ${verification.revocationChecked}`);
|
|
95
|
+
console.log(` certStatus : ${verification.certStatus}`);
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// 4. Download CA chain and install in local trust store
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
100
|
+
console.log('\n[PKI] 4. Downloading CA chain...');
|
|
101
|
+
const chainPem = await client.pki.chain();
|
|
102
|
+
const chainPath = path.join(__dirname, 'certysign-chain.pem');
|
|
103
|
+
fs.writeFileSync(chainPath, chainPem);
|
|
104
|
+
console.log(` CA chain → ${chainPath}`);
|
|
105
|
+
console.log(` (${chainPem.split('-----BEGIN CERTIFICATE-----').length - 1} cert(s) in bundle)`);
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
108
|
+
// 5. Point-in-time verification
|
|
109
|
+
// Used by HIE to check if a cert was valid at the time a document was signed
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
111
|
+
console.log('\n[PKI] 5. Point-in-time verification...');
|
|
112
|
+
const signingTime = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes ago
|
|
113
|
+
const ptResult = await client.certificates.verify(serialNumber, signingTime);
|
|
114
|
+
console.log(` valid at ${signingTime.toISOString()} : ${ptResult.data.valid}`);
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// 6. Fetch and cache CRL (JSON format for application-level checks)
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
119
|
+
console.log('\n[PKI] 6. Fetching CRL (JSON)...');
|
|
120
|
+
const crlJson = await client.pki.crl('json');
|
|
121
|
+
const crl = crlJson.data;
|
|
122
|
+
console.log(` issuer : ${crl.issuer}`);
|
|
123
|
+
console.log(` thisUpdate : ${crl.thisUpdate}`);
|
|
124
|
+
console.log(` nextUpdate : ${crl.nextUpdate}`);
|
|
125
|
+
console.log(` revokedCount : ${crl.revokedCount}`);
|
|
126
|
+
|
|
127
|
+
// Build a Set of revoked serial numbers for fast in-process lookups
|
|
128
|
+
const revokedSet = new Set((crl.crl || []).map(e => e.serialNumber));
|
|
129
|
+
console.log(` Revoked set : ${revokedSet.size} entries loaded`);
|
|
130
|
+
|
|
131
|
+
// Cache on disk until nextUpdate
|
|
132
|
+
fs.writeFileSync(
|
|
133
|
+
path.join(__dirname, 'crl-cache.json'),
|
|
134
|
+
JSON.stringify({ ...crl, cachedAt: new Date().toISOString() }, null, 2)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Also download DER CRL for systems that need binary format
|
|
138
|
+
console.log('\n[PKI] 6b. Downloading DER CRL...');
|
|
139
|
+
const crlDer = await client.pki.crl('der');
|
|
140
|
+
fs.writeFileSync(path.join(__dirname, 'certysign-intermediate.crl'), crlDer);
|
|
141
|
+
console.log(` certysign-intermediate.crl — ${crlDer.length} bytes`);
|
|
142
|
+
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
144
|
+
// 7. In-process revocation check using cached CRL
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
146
|
+
console.log('\n[PKI] 7. Local CRL check...');
|
|
147
|
+
const isRevoked = revokedSet.has(serialNumber);
|
|
148
|
+
console.log(` Serial ${serialNumber.slice(0, 12)}... : ${isRevoked ? 'REVOKED' : 'not in CRL'}`);
|
|
149
|
+
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
151
|
+
// 8. OCSP query — freshest revocation status
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
153
|
+
console.log('\n[PKI] 8. OCSP query...');
|
|
154
|
+
const ocspResult = await client.pki.ocsp(serialNumber, 'json');
|
|
155
|
+
const ocsp = ocspResult.data;
|
|
156
|
+
console.log(` status : ${ocsp.status}`);
|
|
157
|
+
console.log(` producedAt : ${ocsp.producedAt}`);
|
|
158
|
+
console.log(` responderId : ${ocsp.responderId}`);
|
|
159
|
+
console.log(` nextUpdate : ${ocsp.nextUpdate}`);
|
|
160
|
+
console.log(` signatureAlgo : ${ocsp.signatureAlgorithm}`);
|
|
161
|
+
if (ocsp.certIdHash) {
|
|
162
|
+
console.log(` CertID algo : ${ocsp.certIdHash.algorithm}`);
|
|
163
|
+
console.log(` issuerNameHash : ${ocsp.certIdHash.issuerNameHash?.slice(0, 16)}...`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
167
|
+
// 9. CA Info — inspect the trust hierarchy
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
169
|
+
console.log('\n[PKI] 9. CA hierarchy info...');
|
|
170
|
+
const info = await client.pki.info();
|
|
171
|
+
const caInfo = info.data;
|
|
172
|
+
console.log(` Root CA : ${caInfo.rootCA?.subject?.commonName}`);
|
|
173
|
+
console.log(` Root CA expiry : ${caInfo.rootCA?.validity?.notAfter}`);
|
|
174
|
+
console.log(` Inter CA : ${caInfo.intermediateCA?.subject?.commonName}`);
|
|
175
|
+
console.log(` Inter CA expiry: ${caInfo.intermediateCA?.validity?.notAfter}`);
|
|
176
|
+
console.log(` CRL URL : ${caInfo.intermediateCA?.crlUrl}`);
|
|
177
|
+
console.log(` OCSP URL : ${caInfo.intermediateCA?.ocspUrl}`);
|
|
178
|
+
console.log(` Initialised : ${caInfo.status?.initialized}`);
|
|
179
|
+
|
|
180
|
+
console.log('\n[PKI] ✓ Certificate lifecycle demo complete');
|
|
181
|
+
console.log('─'.repeat(60));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
(async () => {
|
|
187
|
+
try {
|
|
188
|
+
await runCertificateFlow();
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (err instanceof CertySignError) {
|
|
191
|
+
console.error(`\n[PKI] API Error (${err.statusCode}): ${err.message}`);
|
|
192
|
+
if (err.details) console.error('[PKI] Details:', JSON.stringify(err.details, null, 2));
|
|
193
|
+
} else {
|
|
194
|
+
console.error('\n[PKI] Error:', err.message);
|
|
195
|
+
}
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
})();
|