node-opcua-pki
Version:
PKI management for node-opcua
1,284 lines (1,280 loc) • 59.9 kB
TypeScript
import { SubjectOptions, CertificatePurpose, Subject, PrivateKey, Certificate, DER, CertificateRevocationList } from 'node-opcua-crypto';
export { Subject, SubjectOptions } from 'node-opcua-crypto';
import { EventEmitter } from 'node:events';
/** RSA key size in bits. */
type KeySize = 1024 | 2048 | 3072 | 4096;
/** Hex-encoded SHA-1 certificate thumbprint. */
type Thumbprint = string;
/** A filesystem path to a file. */
type Filename = string;
/** Status of a certificate in the trust store. */
type CertificateStatus = "unknown" | "trusted" | "rejected";
/**
* @deprecated Use {@link KeySize} instead.
*/
type KeyLength = 1024 | 2048 | 3072 | 4096;
declare function quote(str?: string): string;
/**
* Subject Alternative Name (SAN) parameters for certificate
* generation.
*/
interface ProcessAltNamesParam {
/** DNS host names to include in the SAN extension. */
dns?: string[];
/** IP addresses to include in the SAN extension. */
ip?: string[];
/** OPC UA application URI for the SAN extension. */
applicationUri?: string;
}
/**
* Options for creating a Certificate Signing Request (CSR).
*/
interface CreateCertificateSigningRequestOptions extends ProcessAltNamesParam {
/** X.500 subject for the certificate. */
subject?: SubjectOptions | string;
}
/**
* Extended CSR options that include filesystem paths and
* certificate purpose — used internally by the OpenSSL toolbox.
*/
interface CreateCertificateSigningRequestWithConfigOptions extends CreateCertificateSigningRequestOptions {
/** Root directory of the PKI store. */
rootDir: Filename;
/** Path to the OpenSSL configuration file. */
configFile: Filename;
/** Path to the private key file. */
privateKey: Filename;
/** Intended purpose of the certificate. */
purpose: CertificatePurpose;
}
/**
* Validity period parameters for certificate generation.
*/
interface StartDateEndDateParam {
/** Certificate "Not Before" date. Defaults to now. */
startDate?: Date;
/** Certificate "Not After" date (computed from validity). */
endDate?: Date;
/** Number of days the certificate is valid. @defaultValue 365 */
validity?: number;
/**
* Certificate validity in milliseconds.
*
* When provided, takes precedence over {@link validity} and enables
* sub-day validity (X.509 supports second precision per RFC 5280
* §4.1.2.5; OpenSSL is invoked with `-startdate`/`-enddate` already).
*
* Typical use is short-lived certificates for demos or for renewal
* cycle testing. Existing day-based callers are unaffected.
*/
validityMs?: number;
}
/**
* Parameters for creating a self-signed certificate.
*/
interface CreateSelfSignCertificateParam extends ProcessAltNamesParam, StartDateEndDateParam {
/** X.500 subject for the certificate. */
subject?: SubjectOptions | string;
}
/**
* Extended self-signed certificate options that include
* filesystem paths and purpose — used internally.
*/
interface CreateSelfSignCertificateWithConfigParam extends CreateSelfSignCertificateParam {
/** Root directory of the PKI store. */
rootDir: Filename;
/** Path to the OpenSSL configuration file. */
configFile: Filename;
/** Path to the private key file. */
privateKey: Filename;
/** Intended purpose of the certificate. */
purpose: CertificatePurpose;
}
/**
* General-purpose parameters passed to CA operations such as
* {@link CertificateAuthority.signCertificateRequest} and
* {@link CertificateAuthority.revokeCertificate}.
*/
interface Params extends ProcessAltNamesParam, StartDateEndDateParam {
/** X.500 subject for the certificate. */
subject?: SubjectOptions | string;
/** Path to the private key file. */
privateKey?: string;
/** Path to the OpenSSL configuration file. */
configFile?: string;
/** Root directory of the PKI store. */
rootDir?: string;
/** Output filename for the generated certificate. */
outputFile?: string;
/** CRL revocation reason (e.g. `"keyCompromise"`). */
reason?: string;
}
declare function adjustDate(params: StartDateEndDateParam): void;
declare function adjustApplicationUri(params: Params): void;
/**
* Result of {@link CertificateAuthority.initializeCSR}.
*
* - `"ready"` — the CA certificate already exists and is valid.
* - `"pending"` — key + CSR exist but no cert; waiting for external signing.
* - `"created"` — a fresh key + CSR were just generated.
* - `"expired"` — the CA certificate has expired (or will expire within
* the configured threshold). A new CSR has been generated for renewal
* while preserving the existing private key.
*/
type InitializeCSRResult = {
status: "ready";
} | {
status: "pending";
csrPath: string;
} | {
status: "created";
csrPath: string;
} | {
status: "expired";
csrPath: string;
expiryDate: Date;
};
/**
* Result of {@link CertificateAuthority.installCACertificate}.
*
* - `"success"` — the certificate was installed and CRL generated.
* - `"error"` — the certificate was rejected (see `reason`).
*/
type InstallCACertificateResult = {
status: "success";
} | {
status: "error";
reason: string;
message: string;
};
/**
* Options for creating a {@link CertificateAuthority}.
*/
interface CertificateAuthorityOptions {
/** RSA key size for the CA private key. */
keySize: KeySize;
/** Filesystem path where the CA directory structure is stored. */
location: string;
/**
* X.500 subject for the CA certificate.
* Accepts a slash-delimited string (e.g. `"/CN=My CA/O=Acme"`) or
* a structured {@link SubjectOptions} object.
*
* @defaultValue {@link defaultSubject}
*/
subject?: string | SubjectOptions;
/**
* Parent CA that will sign this CA's certificate.
* If omitted, the CA is self-signed (root CA).
* The parent CA must be initialized before this CA.
*/
issuerCA?: CertificateAuthority;
/**
* Public URL (http/https) where the CRL produced by this CA is
* reachable. When set, every issued certificate carries an
* X.509v3 `crlDistributionPoints` extension pointing at this URL.
*
* Leave undefined to omit the extension entirely (opt-in — see
* US-202). Validated synchronously at construction / setter call.
*/
crlDistributionUrl?: string;
/**
* Public URL of the OCSP responder. When set, every issued cert
* carries an `authorityInfoAccess` extension with an `OCSP` leg
* pointing at this URL. Leave undefined to omit (US-202).
*/
ocspResponderUrl?: string;
/**
* Public URL where the issuer's certificate can be fetched.
* When set, the `authorityInfoAccess` extension on every issued
* cert carries a `caIssuers` leg pointing at this URL (chain
* repair). Leave undefined to omit (US-202).
*/
caIssuersUrl?: string;
}
/**
* An OpenSSL-based Certificate Authority (CA) that can create,
* sign, and revoke X.509 certificates.
*
* The CA maintains a standard OpenSSL directory layout under
* {@link CertificateAuthority.rootDir | rootDir}:
*
* ```
* <location>/
* ├── conf/ OpenSSL configuration
* ├── private/ CA private key (cakey.pem)
* ├── public/ CA certificate (cacert.pem)
* ├── certs/ Signed certificates
* ├── crl/ Revocation lists
* ├── serial Next serial number
* ├── crlnumber Next CRL number
* └── index.txt Certificate database
* ```
*
* @example
* ```ts
* const ca = new CertificateAuthority({
* keySize: 2048,
* location: "/var/pki/CA"
* });
* await ca.initialize();
* ```
*/
/**
* A record from the OpenSSL CA certificate database
* (`index.txt`).
*/
interface IssuedCertificateRecord {
/** Hex-encoded serial number (e.g. `"1000"`). */
serial: string;
/** Certificate status. */
status: "valid" | "revoked" | "expired";
/** X.500 subject string (slash-delimited). */
subject: string;
/** Certificate expiry date as ISO-8601 string. */
expiryDate: string;
/**
* Revocation date as ISO-8601 string.
* Only present when `status === "revoked"`.
*/
revocationDate?: string;
}
/**
* Options for {@link CertificateAuthority.signCertificateRequestFromDER}.
*
* All fields are optional. When provided, they override the
* corresponding values from the CSR.
*/
interface SignCertificateOptions {
/** Certificate validity in days (default: 365). */
validity?: number;
/**
* Certificate validity in milliseconds.
*
* When provided, takes precedence over {@link validity} and enables
* sub-day validity (e.g. 10-minute certificates for renewal demos).
*/
validityMs?: number;
/** Override the certificate start date. */
startDate?: Date;
/** Override DNS SANs. */
dns?: string[];
/** Override IP SANs. */
ip?: string[];
/** Override the application URI SAN. */
applicationUri?: string;
/** Override the X.500 subject. */
subject?: SubjectOptions | string;
}
/**
* Capabilities advertised by a PKI backend (or by this
* {@link CertificateAuthority}) so consumers can clamp requested
* validity to the limits the backend can actually honor.
*
* Useful for the GDS Pull / Push management flows, where the CA may
* be supplied by an external service (step-ca, EJBCA, …) with its
* own minimum / maximum / granularity constraints.
*
* @see CertificateAuthority.getCapabilities
*/
interface PkiBackendCapabilities {
/** Smallest validity this backend can issue, in milliseconds. */
minValidityMs: number;
/** Largest validity this backend will issue, in milliseconds. */
maxValidityMs: number;
/**
* Validity is rounded up to the nearest multiple of this many
* milliseconds. For `node-opcua-pki`'s OpenSSL-based CA this is
* 1 000 ms (one second — the X.509 floor per RFC 5280 §4.1.2.5).
*/
validityGranularityMs: number;
/**
* Native unit the backend works in. Diagnostic only — callers
* always pass `validityMs` (US-208 / US-210).
*/
nativeUnit: "second" | "minute" | "hour" | "day";
}
/**
* Options for {@link CertificateAuthority.generateKeyPairAndSignDER}.
*/
interface GenerateKeyPairAndSignOptions {
/** OPC UA application URI (required). */
applicationUri: string;
/** X.500 subject for the certificate (e.g. "CN=MyApp"). */
subject?: SubjectOptions | string;
/** DNS host names for the SAN extension. */
dns?: string[];
/** IP addresses for the SAN extension. */
ip?: string[];
/** Certificate validity in days (default: 365). */
validity?: number;
/**
* Certificate validity in milliseconds.
*
* When provided, takes precedence over {@link validity} and enables
* sub-day validity (e.g. 10-minute certificates for renewal demos).
*/
validityMs?: number;
/** Certificate start date (default: now). */
startDate?: Date;
/** RSA key size in bits (default: 2048). */
keySize?: KeySize;
}
/**
* Options for {@link CertificateAuthority.generateKeyPairAndSignPFX}.
*
* Extends the DER options with an optional `passphrase` to protect
* the PFX bundle.
*/
interface GenerateKeyPairAndSignPFXOptions extends GenerateKeyPairAndSignOptions {
/**
* Passphrase to protect the PFX file.
* If omitted, the PFX is created without a password.
*/
passphrase?: string;
}
declare class CertificateAuthority {
/** RSA key size used when generating the CA private key. */
readonly keySize: KeySize;
/** Root filesystem path of the CA directory structure. */
readonly location: string;
/** X.500 subject of the CA certificate. */
readonly subject: Subject;
/** @internal Parent CA (undefined for root CAs). */
readonly _issuerCA?: CertificateAuthority;
/** @internal Configured CDP / AIA URLs (US-202). */
private _crlDistributionUrl?;
private _ocspResponderUrl?;
private _caIssuersUrl?;
constructor(options: CertificateAuthorityOptions);
/**
* Public URL where the CRL produced by this CA is reachable, or
* `undefined` if no CDP extension should be emitted on issued certs.
*/
get crlDistributionUrl(): string | undefined;
/**
* Public URL of the OCSP responder, or `undefined` if no AIA OCSP
* leg should be emitted on issued certs.
*/
get ocspResponderUrl(): string | undefined;
/**
* Public URL where the issuer's certificate can be fetched, or
* `undefined` if no AIA caIssuers leg should be emitted.
*/
get caIssuersUrl(): string | undefined;
/**
* Configure the URL embedded as `crlDistributionPoints` in every
* subsequently-issued certificate. Pass `undefined` to disable
* the extension entirely. Validated synchronously — throws on
* empty string, non-http(s) protocol, missing path. Warns (does
* not throw) when the URL points at loopback.
*
* @see US-202
*/
setCrlDistributionUrl(url: string | undefined): void;
/**
* Configure the OCSP responder URL embedded as the `OCSP` leg of
* the `authorityInfoAccess` extension on every subsequently-issued
* certificate. Pass `undefined` to disable.
*
* @see US-202
*/
setOcspResponderUrl(url: string | undefined): void;
/**
* Configure the caIssuers URL embedded as the `caIssuers` leg of
* the `authorityInfoAccess` extension on every subsequently-issued
* certificate. Pass `undefined` to disable.
*
* @see US-202
*/
setCaIssuersUrl(url: string | undefined): void;
/**
* @internal
* Populate the OpenSSL config substitution env vars (`CDP_URL` and
* `AIA_VALUE`) from the configured URLs, or unset them so the
* matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
* stripped. MUST be called before every `generateStaticConfig`
* invocation that signs a certificate.
*/
_wireRevocationEnvVars(): void;
/** Absolute path to the CA root directory (alias for {@link location}). */
get rootDir(): string;
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
get configFile(): string;
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
get caCertificate(): string;
/**
* Path to the issuer certificate chain (`public/issuer_chain.pem`).
*
* This file is created by {@link installCACertificate} when the
* provided cert file contains additional issuer certificates
* (e.g. intermediate + root). It is appended to signed certs
* by {@link constructCertificateChain} to produce a full chain
* per OPC UA Part 6 §6.2.6.
*/
get issuerCertificateChain(): string;
/**
* Path to the current Certificate Revocation List in DER format.
* (`crl/revocation_list.der`)
*/
get revocationListDER(): string;
/**
* Path to the current Certificate Revocation List in PEM format.
* (`crl/revocation_list.crl`)
*/
get revocationList(): string;
/**
* Path to the concatenated CA certificate + CRL file.
* Used by OpenSSL for CRL-based verification.
*/
get caCertificateWithCrl(): string;
/**
* Return the CA certificate as a DER-encoded buffer.
*
* @throws if the CA certificate file does not exist
* (call {@link initialize} first).
*/
getCACertificateDER(): Buffer;
/**
* Return the CA certificate as a PEM-encoded string.
*
* @throws if the CA certificate file does not exist
* (call {@link initialize} first).
*/
getCACertificatePEM(): string;
/**
* Return the current Certificate Revocation List as a
* DER-encoded buffer.
*
* Returns an empty buffer if no CRL has been generated yet.
*/
getCRLDER(): Buffer;
/**
* Return the current Certificate Revocation List as a
* PEM-encoded string.
*
* Returns an empty string if no CRL has been generated yet.
*/
getCRLPEM(): string;
/**
* Return a list of all issued certificates recorded in the
* OpenSSL `index.txt` database.
*
* Each entry includes the serial number, subject, status,
* expiry date, and (for revoked certs) the revocation date.
*/
getIssuedCertificates(): IssuedCertificateRecord[];
/**
* Return the total number of certificates recorded in
* `index.txt`.
*/
getIssuedCertificateCount(): number;
/**
* Return the status of a certificate by its serial number.
*
* @param serial - hex-encoded serial number (e.g. `"1000"`)
* @returns `"valid"`, `"revoked"`, `"expired"`, or
* `undefined` if not found
*/
getCertificateStatus(serial: string): "valid" | "revoked" | "expired" | undefined;
/**
* Read a specific issued certificate by serial number and
* return its content as a DER-encoded buffer.
*
* OpenSSL stores signed certificates in the `certs/`
* directory using the naming convention `<SERIAL>.pem`.
*
* @param serial - hex-encoded serial number (e.g. `"1000"`)
* @returns the DER buffer, or `undefined` if not found
*/
getCertificateBySerial(serial: string): Buffer | undefined;
/**
* Path to the OpenSSL certificate database file.
*/
get indexFile(): string;
/**
* Parse the OpenSSL `index.txt` certificate database.
*
* Each line has tab-separated fields:
* ```
* status expiry [revocationDate] serial unknown subject
* ```
*
* - status: `V` (valid), `R` (revoked), `E` (expired)
* - expiry: `YYMMDDHHmmssZ`
* - revocationDate: present only for revoked certs
* - serial: hex string
* - unknown: always `"unknown"`
* - subject: X.500 slash-delimited string
*/
private _parseIndexTxt;
/**
* Sign a DER-encoded Certificate Signing Request and return
* the signed certificate as a DER buffer.
*
* This method handles temp-file creation and cleanup
* internally so that callers can work with in-memory
* buffers only.
*
* The CA can override fields from the CSR by passing
* `options.dns`, `options.ip`, `options.applicationUri`,
* `options.startDate`, or `options.subject`.
*
* @param csrDer - the CSR as a DER-encoded buffer
* @param options - signing options and CA overrides
* @returns the signed certificate as a DER-encoded buffer
*/
signCertificateRequestFromDER(csrDer: Buffer, options?: SignCertificateOptions): Promise<Buffer>;
/**
* Advertise the validity limits this CA can honor.
*
* Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
* clamp a requested validity against these bounds before calling
* {@link signCertificateRequestFromDER}, so a misconfigured
* `defaultCertValidity` cannot ask the CA for something it cannot
* produce.
*
* Defaults match the OpenSSL-backed implementation:
* - `minValidityMs = 60_000` (1 minute) — practical floor; the
* X.509 spec floor is 1 second but very short certs are rarely
* useful and pathological for any real deployment.
* - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
* enough for root CAs.
* - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
* floor on `notBefore` / `notAfter`.
* - `nativeUnit = "second"` — what `x509Date()` actually encodes.
*
* @see US-208 — the consumer-side capability story.
*/
getCapabilities(): PkiBackendCapabilities;
/**
* Generate a new RSA key pair, create an internal CSR, sign it
* with this CA, and return both the certificate and private key
* as DER-encoded buffers.
*
* The private key is **never stored** by the CA — it exists only
* in a temporary directory that is cleaned up after the operation.
*
* This is used by `StartNewKeyPairRequest` (OPC UA Part 12) for
* constrained devices that cannot generate their own keys.
*
* @param options - key generation and certificate parameters
* @returns `{ certificateDer, privateKey }` — certificate as DER,
* private key as a branded `PrivateKey` buffer
*/
generateKeyPairAndSignDER(options: GenerateKeyPairAndSignOptions): Promise<{
certificateDer: Buffer;
privateKey: PrivateKey;
}>;
/**
* Generate a new RSA key pair, create an internal CSR, sign it
* with this CA, and return the result as a PKCS#12 (PFX)
* buffer bundling the certificate, private key, and CA chain.
*
* The private key is **never stored** by the CA — it exists only
* in a temporary directory that is cleaned up after the operation.
*
* @param options - key generation, certificate, and PFX options
* @returns the PFX as a `Buffer`
*/
generateKeyPairAndSignPFX(options: GenerateKeyPairAndSignPFXOptions): Promise<Buffer>;
/**
* Revoke a DER-encoded certificate and regenerate the CRL.
*
* Extracts the serial number from the certificate, then
* uses the stored cert file at `certs/<serial>.pem` for
* revocation — avoiding temp-file PEM format mismatches.
*
* @param certDer - the certificate as a DER-encoded buffer
* @param reason - CRL reason code
* (default: `"keyCompromise"`)
* @throws if the certificate's serial is not found in the CA
*/
revokeCertificateDER(certDer: Buffer, reason?: string): Promise<void>;
/**
* Initialize the CA directory structure, generate the CA
* private key and self-signed certificate if they do not
* already exist.
*/
initialize(): Promise<void>;
/**
* Initialize the CA directory structure and generate the
* private key + CSR **without signing**.
*
* Use this when the CA certificate will be signed by an
* external (third-party) root CA. After receiving the signed
* certificate, call {@link installCACertificate} to complete
* the setup.
*
* **Idempotent / restart-safe:**
* - If the CA certificate exists and is valid → `{ status: "ready" }`
* - If the CA certificate has expired → `{ status: "expired", csrPath, expiryDate }`
* (a new CSR is generated, preserving the existing private key)
* - If key + CSR exist but no cert (restart before install) →
* `{ status: "pending", csrPath }` without regenerating
* - Otherwise → generates key + CSR → `{ status: "created", csrPath }`
*
* @returns an {@link InitializeCSRResult} describing the CA state
*/
initializeCSR(): Promise<InitializeCSRResult>;
/**
* Check whether the CA certificate needs renewal and, if so,
* generate a new CSR for re-signing by the external root CA.
*
* Use this while the CA is running to detect upcoming expiry
* **before** it actually expires. The existing private key is
* preserved so previously issued certs remain valid.
*
* @param thresholdDays - number of days before expiry at which
* to trigger renewal (default: 30)
* @returns an {@link InitializeCSRResult} — `"expired"` if
* renewal is needed, `"ready"` if the cert is still valid
*/
renewCSR(thresholdDays?: number): Promise<InitializeCSRResult>;
/**
* Generate a CSR using the existing private key.
* @internal
*/
private _generateCSR;
/**
* Install an externally-signed CA certificate and generate
* the initial CRL.
*
* Call this after {@link initializeCSR} once the external
* root CA has signed the CSR.
*
* **Safety checks:**
* - Verifies that the certificate's public key matches the
* CA private key before installing.
*
* @param signedCertFile - path to the PEM-encoded signed
* CA certificate (issued by the external root CA)
* @returns an {@link InstallCACertificateResult} with
* `status: "success"` or `status: "error"` and a `reason`
*/
installCACertificate(signedCertFile: string): Promise<InstallCACertificateResult>;
/**
* Sign a CSR with CA extensions (`v3_ca`), producing a
* subordinate CA certificate.
*
* Unlike {@link signCertificateRequest} which signs with
* end-entity extensions (SANs, etc.), this method signs
* with `basicConstraints = CA:TRUE` and `keyUsage =
* keyCertSign, cRLSign`.
*
* @param certFile - output path for the signed CA cert (PEM)
* @param csrFile - path to the subordinate CA's CSR
* @param params - signing parameters
*/
signCACertificateRequest(certFile: string, csrFile: string, params: {
validity?: number;
}): Promise<void>;
/**
* Rebuild the combined CA certificate + CRL file.
*
* This concatenates the CA certificate with the current
* revocation list so that OpenSSL can verify certificates
* with CRL checking enabled.
*/
constructCACertificateWithCRL(): Promise<void>;
/**
* Append the CA certificate to a signed certificate file,
* creating a PEM certificate chain.
*
* @param certificate - path to the certificate file to extend
*/
constructCertificateChain(certificate: Filename): Promise<void>;
/**
* Create a self-signed certificate using OpenSSL.
*
* @param certificateFile - output path for the signed certificate
* @param privateKey - path to the private key file
* @param params - certificate parameters (subject, validity, SANs)
*/
createSelfSignedCertificate(certificateFile: Filename, privateKey: Filename, params: Params): Promise<void>;
/**
* Revoke a certificate and regenerate the CRL.
*
* @param certificate - path to the certificate file to revoke
* @param params - revocation parameters
* @param params.reason - CRL reason code
* (default `"keyCompromise"`)
*/
revokeCertificate(certificate: Filename, params: Params): Promise<void>;
/**
* Sign a Certificate Signing Request (CSR) with this CA.
*
* The signed certificate is written to `certificate`, and the
* CA certificate chain plus CRL are appended to form a
* complete certificate chain.
*
* @param certificate - output path for the signed certificate
* @param certificateSigningRequestFilename - path to the CSR
* @param params1 - signing parameters (validity, dates, SANs)
* @returns the path to the signed certificate
*/
signCertificateRequest(certificate: Filename, certificateSigningRequestFilename: Filename, params1: Params): Promise<Filename>;
/**
* Verify a certificate against this CA.
*
* @param certificate - path to the certificate file to verify
*/
verifyCertificate(certificate: Filename): Promise<void>;
}
/**
* Identifies which PKI sub-store a certificate event originated from.
*/
type CertificateStore = "trusted" | "rejected" | "issuersCerts";
/**
* Identifies which PKI sub-store a CRL event originated from.
*/
type CrlStore = "crl" | "issuersCrl";
/**
* Events emitted by {@link CertificateManager} when the
* file-system watchers detect certificate or CRL changes.
*/
interface CertificateManagerEvents {
/** A certificate file was added to a store. */
certificateAdded: (event: {
store: CertificateStore;
certificate: Certificate;
fingerprint: string;
filename: string;
}) => void;
/** A certificate file was removed from a store. */
certificateRemoved: (event: {
store: CertificateStore;
fingerprint: string;
filename: string;
}) => void;
/** A certificate file was modified in a store. */
certificateChange: (event: {
store: CertificateStore;
certificate: Certificate;
fingerprint: string;
filename: string;
}) => void;
/** A CRL file was added. */
crlAdded: (event: {
store: CrlStore;
filename: string;
}) => void;
/** A CRL file was removed. */
crlRemoved: (event: {
store: CrlStore;
filename: string;
}) => void;
}
/**
* Options controlling certificate validation in
* {@link CertificateManager.addTrustedCertificateFromChain}.
*
* By default all checks are **strict** (secure). Set individual
* flags to `true` only in test/development environments.
*/
interface AddCertificateValidationOptions {
/**
* Accept certificates whose validity period has expired
* or is not yet active.
* @defaultValue false
*/
acceptExpiredCertificate?: boolean;
/**
* Accept certificates that have been revoked by their
* issuer's CRL. When `false` (the default), a revoked
* certificate is rejected with `BadCertificateRevoked`.
* @defaultValue false
*/
acceptRevokedCertificate?: boolean;
/**
* Do not fail when a CRL is missing for an issuer in the
* chain. When `false` (the default), a missing CRL causes
* `BadCertificateRevocationUnknown`.
* @defaultValue false
*/
ignoreMissingRevocationList?: boolean;
/**
* Maximum depth of the certificate chain (leaf + issuers).
* The leaf certificate counts as depth 1.
* @defaultValue 5
*/
maxChainLength?: number;
}
/**
* Options for creating a {@link CertificateManager}.
*/
interface CertificateManagerOptions {
/**
* RSA key size for generated private keys.
* @defaultValue 2048
*/
keySize?: KeySize;
/** Filesystem path where the PKI directory structure is stored. */
location: string;
/**
* Validation options applied by
* {@link CertificateManager.addTrustedCertificateFromChain}.
*
* Defaults are secure — all checks enabled.
*/
addCertificateValidationOptions?: AddCertificateValidationOptions;
/**
* When `true`, the CertificateManager will **not** start
* chokidar file-system watchers on the PKI folders.
*
* The initial file-system scan still runs so the in-memory
* indexes are populated, but live change detection is
* disabled. This is useful in test / CI environments where
* many CertificateManager instances are created in parallel
* and the accumulated `fs.watch` handles exhaust the libuv
* thread-pool, causing event-loop starvation.
*
* @defaultValue false
*/
disableFileWatchers?: boolean;
}
/**
* Parameters for {@link createSelfSignedCertificate}.
* All fields from {@link CreateSelfSignCertificateParam} are required.
*/
interface CreateSelfSignCertificateParam1 extends CreateSelfSignCertificateParam {
/**
* Output path for the certificate.
* @defaultValue `"own/certs/self_signed_certificate.pem"`
*/
outputFile?: Filename;
/** X.500 subject for the certificate. */
subject: SubjectOptions | string;
/** OPC UA application URI for the SAN extension. */
applicationUri: string;
/** DNS host names to include in the SAN extension. */
dns: string[];
/** Certificate "Not Before" date. */
startDate: Date;
/** Number of days the certificate is valid. */
validity: number;
}
/**
* Options to fine-tune certificate verification behaviour.
* Passed to {@link CertificateManager.verifyCertificate}.
*
* Without any options, `verifyCertificate` is **strict**: only
* certificates that are explicitly present in the trusted store
* will return {@link VerificationStatus.Good}. Unknown or
* rejected certificates return
* {@link VerificationStatus.BadCertificateUntrusted} even when
* their issuer chain is valid.
*
* Set {@link acceptCertificateWithValidIssuerChain} to `true`
* to accept certificates whose issuer chain validates against
* a trusted CA — even if the leaf certificate itself is not
* in the trusted store.
*/
interface VerifyCertificateOptions {
/** Accept certificates whose "Not After" date has passed. */
acceptOutdatedCertificate?: boolean;
/** Accept issuer certificates whose "Not After" date has passed. */
acceptOutDatedIssuerCertificate?: boolean;
/** Do not fail when a CRL is missing for an issuer. */
ignoreMissingRevocationList?: boolean;
/** Accept certificates whose "Not Before" date is in the future. */
acceptPendingCertificate?: boolean;
/**
* Accept a certificate that is not in the trusted store when
* its issuer (CA) certificate is trusted, the signature is
* valid, and the certificate does not appear in the CRL.
*
* When `false` (the default), only certificates explicitly
* placed in the trusted store are accepted — this is the
* same behaviour as {@link CertificateManager.isCertificateTrusted}.
*
* @defaultValue false
*/
acceptCertificateWithValidIssuerChain?: boolean;
}
/**
* OPC UA certificate verification status codes.
*
* These mirror the OPC UA `StatusCode` values for certificate
* validation results.
*/
declare enum VerificationStatus {
/** The certificate provided as a parameter is not valid. */
BadCertificateInvalid = "BadCertificateInvalid",
/** An error occurred verifying security. */
BadSecurityChecksFailed = "BadSecurityChecksFailed",
/** The certificate does not meet the requirements of the security policy. */
BadCertificatePolicyCheckFailed = "BadCertificatePolicyCheckFailed",
/** The certificate has expired or is not yet valid. */
BadCertificateTimeInvalid = "BadCertificateTimeInvalid",
/** An issuer certificate has expired or is not yet valid. */
BadCertificateIssuerTimeInvalid = "BadCertificateIssuerTimeInvalid",
/** The HostName used to connect to a server does not match a HostName in the certificate. */
BadCertificateHostNameInvalid = "BadCertificateHostNameInvalid",
/** The URI specified in the ApplicationDescription does not match the URI in the certificate. */
BadCertificateUriInvalid = "BadCertificateUriInvalid",
/** The certificate may not be used for the requested operation. */
BadCertificateUseNotAllowed = "BadCertificateUseNotAllowed",
/** The issuer certificate may not be used for the requested operation. */
BadCertificateIssuerUseNotAllowed = "BadCertificateIssuerUseNotAllowed",
/** The certificate is not trusted. */
BadCertificateUntrusted = "BadCertificateUntrusted",
/** It was not possible to determine if the certificate has been revoked. */
BadCertificateRevocationUnknown = "BadCertificateRevocationUnknown",
/** It was not possible to determine if the issuer certificate has been revoked. */
BadCertificateIssuerRevocationUnknown = "BadCertificateIssuerRevocationUnknown",
/** The certificate has been revoked. */
BadCertificateRevoked = "BadCertificateRevoked",
/** The issuer certificate has been revoked. */
BadCertificateIssuerRevoked = "BadCertificateIssuerRevoked",
/** The certificate chain is incomplete. */
BadCertificateChainIncomplete = "BadCertificateChainIncomplete",
/** Validation OK. */
Good = "Good"
}
declare function coerceCertificateChain(certificate: Certificate | Certificate[]): Certificate[];
declare function makeFingerprint(certificate: Certificate | Certificate[] | CertificateRevocationList): string;
/**
* Check if the provided certificate acts as an issuer (CA)
* @param certificate - the DER-encoded certificate
* @returns true if the certificate has CA basicConstraints or keyCertSign keyUsage
*/
declare function isIssuer(certificate: Certificate): boolean;
/**
* Check if the provided certificate acts as an intermediate issuer.
* An intermediate issuer is a CA certificate that is not a root CA (not self-signed).
* @param certificate - the DER-encoded certificate
* @returns true if the certificate is a CA and is not self-signed
*/
declare function isIntermediateIssuer(certificate: Certificate): boolean;
/**
* Check if the provided certificate acts as a root issuer.
* A root issuer is a CA certificate that is self-signed.
* @param certificate - the DER-encoded certificate
* @returns true if the certificate is a CA and is self-signed
*/
declare function isRootIssuer(certificate: Certificate): boolean;
/**
* Find the issuer certificate for a given certificate within
* a provided certificate chain.
*
* @param certificate - the DER-encoded certificate whose issuer to find
* @param chain - candidate issuer certificates to search
* @returns the matching issuer certificate, or `null` if not found
*/
declare function findIssuerCertificateInChain(certificate: Certificate | Certificate[], chain: Certificate[]): Certificate | null;
/**
* Lifecycle state of a {@link CertificateManager} instance.
*/
declare enum CertificateManagerState {
Uninitialized = 0,
Initializing = 1,
Initialized = 2,
Disposing = 3,
Disposed = 4
}
/**
* Manages a GDS-compliant PKI directory structure for an OPC UA
* application.
*
* The PKI store layout follows the OPC UA specification:
*
* ```
* <location>/
* ├── own/
* │ ├── certs/ Own certificate(s)
* │ └── private/ Own private key
* ├── trusted/
* │ ├── certs/ Trusted peer certificates
* │ └── crl/ CRLs for trusted certs
* ├── rejected/ Untrusted / rejected certificates
* └── issuers/
* ├── certs/ CA (issuer) certificates
* └── crl/ CRLs for issuer certificates
* ```
*
* File-system watchers keep the in-memory indexes in sync with
* on-disk changes. Call {@link dispose} when the instance is no
* longer needed to release watchers and allow the process to
* exit cleanly.
*
* ## Environment Variables
*
* - **`OPCUA_PKI_USE_POLLING`** — set to `"true"` to use
* polling-based file watching instead of native OS events.
* Useful for NFS, CIFS, Docker volumes, or other remote /
* virtual file systems where native events are unreliable.
*
* - **`OPCUA_PKI_POLLING_INTERVAL`** — polling interval in
* milliseconds (only effective when polling is enabled).
* Clamped to the range [100, 600 000]. Defaults to
* {@link folderPollingInterval} (5 000 ms).
*
* @example
* ```ts
* const cm = new CertificateManager({ location: "/var/pki" });
* await cm.initialize();
* const status = await cm.verifyCertificate(cert);
* await cm.dispose();
* ```
*/
/**
* Status codes returned by {@link CertificateManager.completeCertificateChain}.
*/
declare enum ChainCompletionStatus {
/** The chain already reached a self-signed root — no action was needed. */
AlreadyComplete = "AlreadyComplete",
/** One or more issuer certificates were successfully appended. */
ChainCompleted = "ChainCompleted",
/** The issuer for the last certificate in the chain could not be found
* in the issuers or trusted stores. The chain is still partial. */
IssuerNotFound = "IssuerNotFound",
/** The input chain was empty. */
EmptyChain = "EmptyChain",
/** Chain completion was stopped because the maximum depth was reached. */
MaxDepthReached = "MaxDepthReached"
}
/**
* Result of {@link CertificateManager.completeCertificateChain}.
*/
interface ChainCompletionResult {
/** The (possibly completed) certificate chain, leaf first. */
chain: Certificate[];
/** Status code indicating whether completion succeeded and why/why not. */
status: ChainCompletionStatus;
/** Human-readable diagnostic message. */
message: string;
}
declare class CertificateManager extends EventEmitter {
#private;
/**
* Dispose **all** active CertificateManager instances,
* closing their file watchers and freeing resources.
*
* This is mainly useful in test tear-down to ensure the
* Node.js process can exit cleanly.
*/
static disposeAll(): Promise<void>;
/**
* Assert that all CertificateManager instances have been
* properly disposed. Throws an Error listing the locations
* of any leaked instances.
*
* Intended for use in test `afterAll()` / `afterEach()`
* hooks to catch missing `dispose()` calls early.
*
* @example
* ```ts
* after(() => {
* CertificateManager.checkAllDisposed();
* });
* ```
*/
static checkAllDisposed(): void;
/**
* When `true` (the default), any certificate that is not
* already in the trusted or rejected store is automatically
* written to the rejected folder the first time it is seen.
*/
untrustUnknownCertificate: boolean;
/** Current lifecycle state of this instance. */
state: CertificateManagerState;
/** @deprecated Use {@link folderPollingInterval} instead (typo fix). */
folderPoolingInterval: number;
/** Interval in milliseconds for file-system polling (when enabled). */
get folderPollingInterval(): number;
set folderPollingInterval(value: number);
/** RSA key size used when generating the private key. */
readonly keySize: KeySize;
/**
* Create a new CertificateManager.
*
* The constructor creates the root directory if it does not
* exist but does **not** initialise the PKI store — call
* {@link initialize} before using any other method.
*
* @param options - configuration options
*/
constructor(options: CertificateManagerOptions);
/** Path to the OpenSSL configuration file. */
get configFile(): string;
/** Root directory of the PKI store. */
get rootDir(): string;
/** Path to the private key file (`own/private/private_key.pem`). */
get privateKey(): string;
/** Path to the OpenSSL random seed file. */
get randomFile(): string;
/**
* Move a certificate to the rejected store.
* If the certificate was previously trusted, it will be removed from the trusted folder.
* @param certificateOrChain - the DER-encoded certificate or certificate chain
*/
rejectCertificate(certificateOrChain: Certificate | Certificate[]): Promise<void>;
/**
* Move a certificate to the trusted store.
* If the certificate was previously rejected, it will be removed from the rejected folder.
* @param certificateOrChain - the DER-encoded certificate or certificate chain
*/
trustCertificate(certificateOrChain: Certificate | Certificate[]): Promise<void>;
/**
* Check whether the trusted certificate store is empty.
*
* This inspects the in-memory index, which is kept in
* sync with the `trusted/certs/` folder by file-system
* watchers after {@link initialize} has been called.
*/
isTrustListEmpty(): boolean;
/**
* Return the number of certificates currently in the
* trusted store.
*/
getTrustedCertificateCount(): number;
/** Path to the rejected certificates folder. */
get rejectedFolder(): string;
/** Path to the trusted certificates folder. */
get trustedFolder(): string;
/** Path to the trusted CRL folder. */
get crlFolder(): string;
/** Path to the issuer (CA) certificates folder. */
get issuersCertFolder(): string;
/** Path to the issuer CRL folder. */
get issuersCrlFolder(): string;
/** Path to the own certificate folder. */
get ownCertFolder(): string;
get ownPrivateFolder(): string;
/**
* Check if a certificate is in the trusted store.
* If the certificate is unknown and `untrustUnknownCertificate` is set,
* it will be written to the rejected folder.
* @param certificate - the DER-encoded certificate
* @returns `"Good"` if trusted, `"BadCertificateUntrusted"` if rejected/unknown,
* or `"BadCertificateInvalid"` if the certificate cannot be parsed.
*/
isCertificateTrusted(certificateOrCertificateChain: Certificate | Certificate[]): Promise<"Good" | "BadCertificateUntrusted" | "BadCertificateInvalid">;
/**
* Internal verification hook called by {@link verifyCertificate}.
*
* Subclasses can override this to inject additional validation
* logic (e.g. application-level policy checks) while still
* delegating to the default chain/CRL/trust verification.
*
* @param certificate - the DER-encoded certificate to verify
* @param options - verification options forwarded from the
* public API
* @returns the verification status code
*/
protected verifyCertificateAsync(certificate: Certificate | Certificate[], options: VerifyCertificateOptions): Promise<VerificationStatus>;
/**
* Verify a certificate against the PKI trust store.
*
* This performs a full validation including trust status,
* issuer chain, CRL revocation checks, and time validity.
*
* @param certificate - the DER-encoded certificate to verify
* @param options - optional flags to relax validation rules
* @returns the verification status code
*/
verifyCertificate(certificate: Certificate | Certificate[], options?: VerifyCertificateOptions): Promise<VerificationStatus>;
/**
* Initialize the PKI directory structure, generate the
* private key (if missing), and start file-system watchers.
*
* This method is idempotent — subsequent calls are no-ops.
* It must be called before any certificate operations.
*/
initialize(): Promise<void>;
/**
* Dispose of the CertificateManager, releasing file watchers
* and other resources. The instance should not be used after
* calling this method.
*/
dispose(): Promise<void>;
/**
* Force a full re-scan of all PKI folders, rebuilding
* the in-memory `_thumbs` index from scratch.
*
* Call this after external processes have modified the
* PKI folders (e.g. via `writeTrustList` or CLI tools)
* to ensure the CertificateManager sees the latest
* state without waiting for file-system events.
*/
reloadCertificates(): Promise<void>;
protected withLock2<T>(action: () => Promise<T>): Promise<T>;
/**
* Create a self-signed certificate for this PKI's private key.
*
* The certificate is written to `params.outputFile` or
* `own/certs/self_signed_certificate.pem` by default.
*
* @param params - certificate parameters (subject, SANs,
* validity, etc.)
*/
createSelfSignedCertificate(params: CreateSelfSignCertificateParam1): Promise<void>;
/**
* Create a Certificate Signing Request (CSR) using this
* PKI's private key and configuration.
*
* The CSR file is written to `own/certs/` with a timestamped
* filename.
*
* @param params - CSR parameters (subject, SANs)
* @returns the filesystem path to the generated CSR file
*/
createCertificateRequest(params: CreateSelfSignCertificateParam): Promise<Filename>;
/**
* Add a CA (issuer) certificate to the issuers store.
* If the certificate is already present, this is a no-op.
* @param certificate - the DER-encoded CA certificate
* @param validate - if `true`, verify the certificate before adding
* @param addInTrustList - if `true`, also add to the trusted store
* @returns `VerificationStatus.Good` on success
*/
addIssuer(certificate: DER, validate?: boolean, addInTrustList?: boolean): Promise<VerificationStatus>;
/**
* Add multiple CA (issuer) certificates to the issuers store.
* @param certificates - the DER-encoded CA certificates
* @param validate - if `true`, verify each certificate before adding
* @param addInTrustList - if `true`, also add each certificate to the trusted store
* @returns `VerificationStatus.Good` on success
*/
addIssuers(certificates: Certificate[], validate?: boolean, addInTrustList?: boolean): Promise<VerificationStatus>;
/**
* Add a CRL to the certificate manager.
* @param crl - the CRL to add
* @param target - "issuers" (default) writes to issuers/crl, "trusted" writes to trusted/crl
*/
addRevocationList(crl: CertificateRevocationList, target?: "issuers" | "trusted"): Promise<VerificationStatus>;
/**
* Remove all CRL files from the specified folder(s) and clear the
* corresponding in-memory index.
* @param target - "issuers" clears issuers/crl, "trusted" clears
* trusted/crl, "all" clears both.
*/
clearRevocationLists(target: "issuers" | "trusted" | "all"): Promise<void>;
/**
* Check whether an issuer certificate with the given thumbprint
* is already registered.
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
*/
hasIssuer(thumbprint: string): Promise<boolean>;
/**
* Remove a trusted certificate identified by its SHA-1 thumbprint.
* Deletes the file on disk and removes the entry from the
* in-memory index.
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
* @returns the removed certificate buffer, or `null` if not found
*/
removeTrustedCertificate(thumbprint: string): Promise<Certificate | null>;
/**
* Remove an issuer certificate identified by its SHA-1 thumbprint.
* Deletes the file on disk and removes the entry from the
* in-memory index.
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
* @returns the removed certificate buffer, or `null` if not found
*/
removeIssuer(thumbprint: string): Promise<Certificate | null>;
/**
* Remove all CRL files that were issued by the given CA certificate
* from the specified folder (or both).
* @param issuerCertificate - the CA certificate whose CRLs to remove
* @param target - "issuers",