Skip to content

Commit 1edbc94

Browse files
committed
refactor: complete Certificate[] array migration in node-opcua-pki
Update CertificateManager to accept Certificate[] (certificate chain) instead of a single Certificate buffer across its validation and management methods. Update all related tests to pass an array of certificates (chain) to verifyCertificate, trustCertificate, and rejectCertificate methods to align with the new architecture. This completes the migration away from single certificate buffers to full certificate chains in the PKI management components.
1 parent f9f56cb commit 1edbc94

12 files changed

Lines changed: 314 additions & 252 deletions

packages/node-opcua-pki/docs/certificate-manager.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ If the CA is **only** in `issuers/certs`, the result is
201201
```typescript
202202
import { readCertificate, readCertificateRevocationList } from "node-opcua-crypto";
203203

204-
const caCert = readCertificate("path/to/ca_cert.der");
204+
const caCert = readCertificateChain("path/to/ca_cert.der");
205205
const caCrl = readCertificateRevocationList("path/to/ca_crl.crl");
206206

207207
// Step 1: Add to issuers store (chain building)

packages/node-opcua-pki/lib/pki/certificate_manager.ts

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import {
2424
exploreCertificateRevocationList,
2525
generatePrivateKeyFile,
2626
makeSHA1Thumbprint,
27-
readCertificate,
28-
readCertificateAsync,
27+
readCertificateChain,
28+
readCertificateChainAsync,
2929
readCertificateRevocationList,
3030
split_der,
3131
toPem,
@@ -326,16 +326,17 @@ function isSelfSigned3(certificate: Buffer): boolean {
326326
* @param chain - candidate issuer certificates to search
327327
* @returns the matching issuer certificate, or `null` if not found
328328
*/
329-
export function findIssuerCertificateInChain(certificate: Certificate, chain: Certificate[]): Certificate | null {
330-
if (!certificate) {
329+
export function findIssuerCertificateInChain(certificate: Certificate | Certificate[], chain: Certificate[]): Certificate | null {
330+
const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
331+
if (!firstCertificate) {
331332
return null;
332333
}
333-
const certInfo = exploreCertificateCached(certificate);
334+
const certInfo = exploreCertificateCached(firstCertificate);
334335

335336
// istanbul ignore next
336337
if (isSelfSigned2(certInfo)) {
337338
// the certificate is self signed so is it's own issuer.
338-
return certificate;
339+
return firstCertificate;
339340
}
340341
const wantedIssuerKey = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
341342

@@ -650,7 +651,9 @@ export class CertificateManager extends EventEmitter {
650651
* @returns `"Good"` if trusted, `"BadCertificateUntrusted"` if rejected/unknown,
651652
* or `"BadCertificateInvalid"` if the certificate cannot be parsed.
652653
*/
653-
public async isCertificateTrusted(certificate: Certificate): Promise<string> {
654+
public async isCertificateTrusted(
655+
certificate: Certificate
656+
): Promise<"Good" | "BadCertificateUntrusted" | "BadCertificateInvalid"> {
654657
const fingerprint = makeFingerprint(certificate) as Thumbprint;
655658

656659
if (this.#thumbs.trusted.has(fingerprint)) {
@@ -675,7 +678,7 @@ export class CertificateManager extends EventEmitter {
675678
return "BadCertificateUntrusted";
676679
}
677680
async #innerVerifyCertificateAsync(
678-
certificate: Certificate,
681+
certificateOrChain: Certificate | Certificate[],
679682
_isIssuer: boolean,
680683
level: number,
681684
options: VerifyCertificateOptions
@@ -684,7 +687,7 @@ export class CertificateManager extends EventEmitter {
684687
// maximum level of certificate in chain reached !
685688
return VerificationStatus.BadSecurityChecksFailed;
686689
}
687-
const chain = split_der(certificate);
690+
const chain = Array.isArray(certificateOrChain) ? certificateOrChain : [certificateOrChain];
688691
debugLog("NB CERTIFICATE IN CHAIN = ", chain.length);
689692
const info = exploreCertificateCached(chain[0]);
690693

@@ -731,7 +734,7 @@ export class CertificateManager extends EventEmitter {
731734
return VerificationStatus.BadCertificateIssuerRevocationUnknown;
732735
}
733736
if (issuerStatus === VerificationStatus.BadCertificateTimeInvalid) {
734-
if (!options || !options.acceptOutDatedIssuerCertificate) {
737+
if (!options?.acceptOutDatedIssuerCertificate) {
735738
// the issuer must have valid dates ....
736739
return VerificationStatus.BadCertificateIssuerTimeInvalid;
737740
}
@@ -746,15 +749,15 @@ export class CertificateManager extends EventEmitter {
746749
return VerificationStatus.BadSecurityChecksFailed;
747750
}
748751
// verify that certificate was signed by issuer
749-
const isCertificateSignatureOK = verifyCertificateSignature(certificate, issuerCertificate);
752+
const isCertificateSignatureOK = verifyCertificateSignature(chain[0], issuerCertificate);
750753
if (!isCertificateSignatureOK) {
751754
debugLog(" the certificate was not signed by the issuer as it claim to be ! Danger");
752755
return VerificationStatus.BadSecurityChecksFailed;
753756
}
754757
hasValidIssuer = true;
755758

756759
// let detected if our certificate is in the revocation list
757-
let revokedStatus = await this.isCertificateRevoked(certificate);
760+
let revokedStatus = await this.isCertificateRevoked(certificateOrChain);
758761
if (revokedStatus === VerificationStatus.BadCertificateRevocationUnknown) {
759762
if (options?.ignoreMissingRevocationList) {
760763
// continue as if the certificate was not revoked
@@ -781,17 +784,17 @@ export class CertificateManager extends EventEmitter {
781784
}
782785
} else {
783786
// verify that certificate was signed by issuer (self in this case)
784-
const isCertificateSignatureOK = verifyCertificateSignature(certificate, certificate);
787+
const isCertificateSignatureOK = verifyCertificateSignature(chain[0], chain[0]);
785788
if (!isCertificateSignatureOK) {
786789
debugLog("Self-signed Certificate signature is not valid");
787790
return VerificationStatus.BadSecurityChecksFailed;
788791
}
789-
const revokedStatus = await this.isCertificateRevoked(certificate);
792+
const revokedStatus = await this.isCertificateRevoked(certificateOrChain);
790793
debugLog("revokedStatus of self signed certificate:", revokedStatus);
791794
}
792795
}
793796

794-
const status = await this.#checkRejectedOrTrusted(certificate);
797+
const status = await this.#checkRejectedOrTrusted(certificateOrChain);
795798
if (status === "rejected") {
796799
if (!(options.acceptCertificateWithValidIssuerChain && hasValidIssuer && hasTrustedIssuer)) {
797800
return VerificationStatus.BadCertificateUntrusted;
@@ -802,7 +805,7 @@ export class CertificateManager extends EventEmitter {
802805

803806
// Has SoftwareCertificate passed its issue date and has it not expired ?
804807
// check dates
805-
const certificateInfo = exploreCertificateInfo(certificate);
808+
const certificateInfo = exploreCertificateInfo(chain[0]);
806809
const now = new Date();
807810

808811
let isTimeInvalid = false;
@@ -861,9 +864,26 @@ export class CertificateManager extends EventEmitter {
861864
* @returns the verification status code
862865
*/
863866
protected async verifyCertificateAsync(
864-
certificate: Certificate,
867+
certificate: Certificate | Certificate[],
865868
options: VerifyCertificateOptions
866869
): Promise<VerificationStatus> {
870+
// When the input is a single buffer, validate that every
871+
// DER element it contains is a valid certificate. A buffer
872+
// with trailing non-certificate data (e.g. a CRL appended
873+
// after a certificate) must be rejected early.
874+
if (!Array.isArray(certificate)) {
875+
try {
876+
const derElements = split_der(certificate);
877+
for (const element of derElements) {
878+
// exploreCertificateInfo will throw if the DER
879+
// element is not a valid X.509 certificate
880+
// (e.g. it is a CRL or other ASN.1 structure).
881+
exploreCertificateInfo(element);
882+
}
883+
} catch (_err) {
884+
return VerificationStatus.BadCertificateInvalid;
885+
}
886+
}
867887
const status1 = await this.#innerVerifyCertificateAsync(certificate, false, 0, options);
868888
return status1;
869889
}
@@ -878,7 +898,10 @@ export class CertificateManager extends EventEmitter {
878898
* @param options - optional flags to relax validation rules
879899
* @returns the verification status code
880900
*/
881-
public async verifyCertificate(certificate: Certificate, options?: VerifyCertificateOptions): Promise<VerificationStatus> {
901+
public async verifyCertificate(
902+
certificate: Certificate | Certificate[],
903+
options?: VerifyCertificateOptions
904+
): Promise<VerificationStatus> {
882905
// Is the signature on the SoftwareCertificate valid .?
883906
if (!certificate) {
884907
// missing certificate
@@ -1141,6 +1164,20 @@ export class CertificateManager extends EventEmitter {
11411164
return VerificationStatus.Good;
11421165
}
11431166

1167+
/**
1168+
* Add multiple CA (issuer) certificates to the issuers store.
1169+
* @param certificates - the DER-encoded CA certificates
1170+
* @param validate - if `true`, verify each certificate before adding
1171+
* @param addInTrustList - if `true`, also add each certificate to the trusted store
1172+
* @returns `VerificationStatus.Good` on success
1173+
*/
1174+
public async addIssuers(certificates: Certificate[], validate = false, addInTrustList = false): Promise<VerificationStatus> {
1175+
for (const certificate of certificates) {
1176+
await this.addIssuer(certificate, validate, addInTrustList);
1177+
}
1178+
return VerificationStatus.Good;
1179+
}
1180+
11441181
/**
11451182
* Add a CRL to the certificate manager.
11461183
* @param crl - the CRL to add
@@ -1328,8 +1365,8 @@ export class CertificateManager extends EventEmitter {
13281365
* @returns `VerificationStatus.Good` on success, or an error
13291366
* status indicating why the certificate was rejected.
13301367
*/
1331-
public async addTrustedCertificateFromChain(certificateChain: Certificate): Promise<VerificationStatus> {
1332-
const certificates = split_der(certificateChain);
1368+
public async addTrustedCertificateFromChain(certificateChain: Certificate | Certificate[]): Promise<VerificationStatus> {
1369+
const certificates = Array.isArray(certificateChain) ? certificateChain : split_der(certificateChain);
13331370
const leafCertificate = certificates[0];
13341371

13351372
// Structural validation — can we parse it?
@@ -1412,12 +1449,13 @@ export class CertificateManager extends EventEmitter {
14121449
* if found. If multiple matching certificates are found, a warning is logged to the console.
14131450
*
14141451
*/
1415-
public async findIssuerCertificate(certificate: Certificate): Promise<Certificate | null> {
1416-
const certInfo = exploreCertificateCached(certificate);
1452+
public async findIssuerCertificate(certificate: Certificate | Certificate[]): Promise<Certificate | null> {
1453+
const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
1454+
const certInfo = exploreCertificateCached(firstCertificate);
14171455

14181456
if (isSelfSigned2(certInfo)) {
14191457
// the certificate is self signed so is it's own issuer.
1420-
return certificate;
1458+
return firstCertificate;
14211459
}
14221460

14231461
const wantedIssuerKey = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
@@ -1459,8 +1497,9 @@ export class CertificateManager extends EventEmitter {
14591497
* @internal
14601498
* @private
14611499
*/
1462-
async #checkRejectedOrTrusted(certificate: Buffer): Promise<CertificateStatus> {
1463-
const fingerprint = makeFingerprint(certificate);
1500+
async #checkRejectedOrTrusted(certificate: Certificate | Certificate[]): Promise<CertificateStatus> {
1501+
const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
1502+
const fingerprint = makeFingerprint(firstCertificate);
14641503

14651504
debugLog("#checkRejectedOrTrusted fingerprint ", short(fingerprint));
14661505

@@ -1538,15 +1577,16 @@ export class CertificateManager extends EventEmitter {
15381577
* found.
15391578
*/
15401579
public async isCertificateRevoked(
1541-
certificate: Certificate,
1580+
certificate: Certificate | Certificate[],
15421581
issuerCertificate?: Certificate | null
15431582
): Promise<VerificationStatus> {
1544-
if (isSelfSigned3(certificate)) {
1583+
const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
1584+
if (isSelfSigned3(firstCertificate)) {
15451585
return VerificationStatus.Good;
15461586
}
15471587

15481588
if (!issuerCertificate) {
1549-
issuerCertificate = await this.findIssuerCertificate(certificate);
1589+
issuerCertificate = await this.findIssuerCertificate(firstCertificate);
15501590
}
15511591
if (!issuerCertificate) {
15521592
return VerificationStatus.BadCertificateChainIncomplete;
@@ -1556,7 +1596,7 @@ export class CertificateManager extends EventEmitter {
15561596
if (!crls) {
15571597
return VerificationStatus.BadCertificateRevocationUnknown;
15581598
}
1559-
const certInfo = exploreCertificateCached(certificate);
1599+
const certInfo = exploreCertificateCached(firstCertificate);
15601600
const serialNumber =
15611601
certInfo.tbsCertificate.serialNumber || certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.serial || "";
15621602

@@ -1750,7 +1790,7 @@ export class CertificateManager extends EventEmitter {
17501790
try {
17511791
const stat = await fs.promises.stat(filename);
17521792
if (!stat.isFile()) continue;
1753-
const certificate = await readCertificateAsync(filename);
1793+
const certificate = (await readCertificateChainAsync(filename))[0];
17541794
const info = exploreCertificateCached(certificate);
17551795
const fingerprint = makeFingerprint(certificate);
17561796
index.set(fingerprint, { certificate, filename, info });
@@ -1851,7 +1891,7 @@ export class CertificateManager extends EventEmitter {
18511891
w.on("add", (filename: string) => {
18521892
debugLog(chalk.cyan(`add in folder ${folder}`), filename);
18531893
try {
1854-
const certificate = readCertificate(filename);
1894+
const certificate = readCertificateChain(filename)[0];
18551895
const info = exploreCertificateCached(certificate);
18561896
const fingerprint = makeFingerprint(certificate);
18571897

@@ -1876,7 +1916,7 @@ export class CertificateManager extends EventEmitter {
18761916
w.on("change", (changedPath: string) => {
18771917
debugLog(chalk.cyan(`change in folder ${folder}`), changedPath);
18781918
try {
1879-
const certificate = readCertificate(changedPath);
1919+
const certificate = readCertificateChain(changedPath)[0];
18801920
const newFingerprint = makeFingerprint(certificate);
18811921
const oldHash = this.#filenameToHash.get(changedPath);
18821922
if (oldHash && oldHash !== newFingerprint) {

test/test_advanced_certificate_validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from "node:path";
33
import sinon from "sinon";
44
import "should";
55

6-
import { readCertificate } from "node-opcua-crypto";
6+
import { readCertificateChain } from "node-opcua-crypto";
77
import { CertificateManager, type CertificateManagerOptions } from "node-opcua-pki";
88
import { beforeTest } from "./helpers";
99

@@ -34,7 +34,7 @@ describe("Check Validate Certificate", function () {
3434
const cert2Filename = path.join(__dirname, "fixtures/CTT_sample_certificates/CA/certs/ctt_ca1I_appTR.der");
3535

3636
// installing the CA certificate
37-
const caCert = readCertificate(caCertificateFilename);
37+
const caCert = readCertificateChain(caCertificateFilename)[0];
3838
let caCertStatus = await certificateManager.verifyCertificate(caCert);
3939
caCertStatus.should.eql("BadCertificateUntrusted");
4040

0 commit comments

Comments
 (0)