node-opcua-crypto 2.1.2 → 3.0.0-beta.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/.fossa.yml +18 -18
- package/.github/FUNDING.yml +12 -12
- package/.github/workflows/main.yml +109 -106
- package/.prettierrc.js +6 -6
- package/LICENSE +23 -23
- package/README.md +14 -14
- package/_tmp_certificate.pem +20 -0
- package/_tmp_csr.pem +17 -0
- package/_tmp_privatekey.der +0 -0
- package/_tmp_privatekey.pem +28 -0
- package/dist/source/asn1.d.ts +73 -73
- package/dist/source/asn1.js +359 -359
- package/dist/source/asn1.js.map +1 -1
- package/dist/source/buffer_utils.d.ts +5 -6
- package/dist/source/buffer_utils.js +21 -21
- package/dist/source/common.d.ts +20 -14
- package/dist/source/common.js +10 -2
- package/dist/source/common.js.map +1 -1
- package/dist/source/create_key_pair.d.ts +42 -0
- package/dist/source/create_key_pair.js +136 -0
- package/dist/source/create_key_pair.js.map +1 -0
- package/dist/source/crypto_explore_certificate.d.ts +107 -107
- package/dist/source/crypto_explore_certificate.js +601 -601
- package/dist/source/crypto_utils.d.ts +76 -76
- package/dist/source/crypto_utils.js +329 -329
- package/dist/source/derived_keys.d.ts +72 -72
- package/dist/source/derived_keys.js +248 -248
- package/dist/source/explore_certificate.d.ts +30 -30
- package/dist/source/explore_certificate.js +43 -43
- package/dist/source/explore_certificate_revocation_list.d.ts +28 -28
- package/dist/source/explore_certificate_revocation_list.js +69 -69
- package/dist/source/explore_certificate_signing_request.d.ts +13 -13
- package/dist/source/explore_certificate_signing_request.js +44 -44
- package/dist/source/explore_private_key.d.ts +29 -29
- package/dist/source/explore_private_key.js +95 -97
- package/dist/source/explore_private_key.js.map +1 -1
- package/dist/source/index.d.ts +17 -13
- package/dist/source/index.js +33 -29
- package/dist/source/index.js.map +1 -1
- package/dist/source/oid_map.d.ts +7 -7
- package/dist/source/oid_map.js +303 -303
- package/dist/source/public_private_match.d.ts +3 -3
- package/dist/source/public_private_match.js +36 -36
- package/dist/source/subject.d.ts +27 -0
- package/dist/source/subject.js +125 -0
- package/dist/source/subject.js.map +1 -0
- package/dist/source/verify_certificate_signature.d.ts +10 -10
- package/dist/source/verify_certificate_signature.js +101 -101
- package/dist/source/x509/_build_public_key.d.ts +1 -0
- package/dist/source/x509/_build_public_key.js +36 -0
- package/dist/source/x509/_build_public_key.js.map +1 -0
- package/dist/source/x509/_crypto.d.ts +3 -0
- package/dist/source/x509/_crypto.js +9 -0
- package/dist/source/x509/_crypto.js.map +1 -0
- package/dist/source/x509/_fix.d.ts +2 -0
- package/dist/source/x509/_fix.js +74 -0
- package/dist/source/x509/_fix.js.map +1 -0
- package/dist/source/x509/_get_attributes.d.ts +8 -0
- package/dist/source/x509/_get_attributes.js +56 -0
- package/dist/source/x509/_get_attributes.js.map +1 -0
- package/dist/source/x509/_internals.d.ts +0 -0
- package/dist/source/x509/_internals.js +2 -0
- package/dist/source/x509/_internals.js.map +1 -0
- package/dist/source/x509/create_certificate_signing_request.d.ts +18 -0
- package/dist/source/x509/create_certificate_signing_request.js +53 -0
- package/dist/source/x509/create_certificate_signing_request.js.map +1 -0
- package/dist/source/x509/create_key_pair.d.ts +28 -0
- package/dist/source/x509/create_key_pair.js +62 -0
- package/dist/source/x509/create_key_pair.js.map +1 -0
- package/dist/source/x509/create_self_signed_certificate.d.ts +17 -0
- package/dist/source/x509/create_self_signed_certificate.js +71 -0
- package/dist/source/x509/create_self_signed_certificate.js.map +1 -0
- package/dist/source_nodejs/generate_private_key_filename.d.ts +1 -0
- package/dist/source_nodejs/generate_private_key_filename.js +25 -0
- package/dist/source_nodejs/generate_private_key_filename.js.map +1 -0
- package/dist/source_nodejs/index.d.ts +4 -3
- package/dist/source_nodejs/index.js +20 -19
- package/dist/source_nodejs/index.js.map +1 -1
- package/dist/source_nodejs/read.d.ts +23 -23
- package/dist/source_nodejs/read.js +106 -106
- package/dist/source_nodejs/read_certificate_revocation_list.d.ts +2 -2
- package/dist/source_nodejs/read_certificate_revocation_list.js +27 -27
- package/dist/source_nodejs/read_certificate_signing_request.d.ts +3 -3
- package/dist/source_nodejs/read_certificate_signing_request.js +27 -27
- package/index.d.ts +2 -2
- package/index.js +4 -4
- package/index_web.js +3 -3
- package/package.json +15 -9
- package/source/asn1.ts +404 -404
- package/source/buffer_utils.ts +18 -18
- package/source/common.ts +7 -0
- package/source/crypto_explore_certificate.ts +764 -764
- package/source/derived_keys.ts +287 -287
- package/source/explore_certificate.ts +66 -66
- package/source/explore_certificate_revocation_list.ts +122 -122
- package/source/explore_certificate_signing_request.ts +58 -58
- package/source/explore_private_key.ts +1 -2
- package/source/index.ts +17 -13
- package/source/oid_map.ts +310 -310
- package/source/subject.ts +144 -0
- package/source/verify_certificate_signature.ts +105 -105
- package/source/x509/_build_public_key.ts +25 -0
- package/source/x509/_crypto.ts +5 -0
- package/source/x509/_get_attributes.ts +60 -0
- package/source/x509/create_certificate_signing_request.ts +64 -0
- package/source/x509/create_key_pair.ts +70 -0
- package/source/x509/create_self_signed_certificate.ts +91 -0
- package/source_nodejs/generate_private_key_filename.ts +10 -0
- package/source_nodejs/index.ts +4 -3
- package/source_nodejs/read_certificate_revocation_list.ts +14 -14
- package/source_nodejs/read_certificate_signing_request.ts +17 -17
- package/test_certificate.ts +34 -34
- package/tsconfig.json +19 -18
- package/tslint.json +34 -34
- package/dist/source/certificate_matches_private_key.d.ts +0 -2
- package/dist/source/certificate_matches_private_key.js +0 -22
- package/dist/source/certificate_matches_private_key.js.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// node-opcua-pki
|
|
3
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
// Copyright (c) 2014-2022 - Etienne Rossignon - etienne.rossignon (at) gadz.org
|
|
5
|
+
// Copyright (c) 2022 - Sterfive.com
|
|
6
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
7
|
+
//
|
|
8
|
+
// This project is licensed under the terms of the MIT license.
|
|
9
|
+
//
|
|
10
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
11
|
+
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
12
|
+
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
13
|
+
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
14
|
+
//
|
|
15
|
+
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
16
|
+
// Software.
|
|
17
|
+
//
|
|
18
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
19
|
+
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
20
|
+
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
21
|
+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface SubjectOptions {
|
|
25
|
+
commonName?: string;
|
|
26
|
+
organization?: string;
|
|
27
|
+
organizationalUnit?: string;
|
|
28
|
+
locality?: string;
|
|
29
|
+
state?: string;
|
|
30
|
+
country?: string;
|
|
31
|
+
domainComponent?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const _keys = {
|
|
35
|
+
C: "country",
|
|
36
|
+
CN: "commonName",
|
|
37
|
+
DC: "domainComponent",
|
|
38
|
+
L: "locality",
|
|
39
|
+
O: "organization",
|
|
40
|
+
OU: "organizationalUnit",
|
|
41
|
+
ST: "state",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const enquoteIfNecessary = (str: string) => {
|
|
45
|
+
str = str.replace(/"/g, "”");
|
|
46
|
+
return str.match(/\/|=/) ? `"${str}"` : str;
|
|
47
|
+
};
|
|
48
|
+
const unquote = (str: string) => str.replace(/"/gm, "");
|
|
49
|
+
const unquote2 = (str?: string | undefined) => {
|
|
50
|
+
if (!str) return str;
|
|
51
|
+
const m = str.match(/^"(.*)"$/);
|
|
52
|
+
return m ? m[1] : str;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* subjectName The subject name to use for the Certificate.
|
|
56
|
+
* If not specified the ApplicationName and/or domainNames are used to create a suitable default value.
|
|
57
|
+
*/
|
|
58
|
+
export class Subject implements SubjectOptions {
|
|
59
|
+
public readonly commonName?: string;
|
|
60
|
+
public readonly organization?: string;
|
|
61
|
+
public readonly organizationalUnit?: string;
|
|
62
|
+
public readonly locality?: string;
|
|
63
|
+
public readonly state?: string;
|
|
64
|
+
public readonly country?: string;
|
|
65
|
+
public readonly domainComponent?: string;
|
|
66
|
+
|
|
67
|
+
constructor(options: SubjectOptions | string) {
|
|
68
|
+
if (typeof options === "string") {
|
|
69
|
+
options = Subject.parse(options);
|
|
70
|
+
}
|
|
71
|
+
this.commonName = unquote2(options.commonName);
|
|
72
|
+
this.organization = unquote2(options.organization);
|
|
73
|
+
this.organizationalUnit = unquote2(options.organizationalUnit);
|
|
74
|
+
this.locality = unquote2(options.locality);
|
|
75
|
+
this.state = unquote2(options.state);
|
|
76
|
+
this.country = unquote2(options.country);
|
|
77
|
+
this.domainComponent = unquote2(options.domainComponent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public static parse(str: string): SubjectOptions {
|
|
81
|
+
const elements = str.split(/\/(?=[^/]*?=)/);
|
|
82
|
+
const options: Record<string, unknown> = {};
|
|
83
|
+
|
|
84
|
+
elements.forEach((element: string) => {
|
|
85
|
+
if (element.length === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const s: string[] = element.split("=");
|
|
89
|
+
|
|
90
|
+
if (s.length !== 2) {
|
|
91
|
+
throw new Error("invalid format for " + element);
|
|
92
|
+
}
|
|
93
|
+
const longName = (_keys as Record<string, string>)[s[0]];
|
|
94
|
+
if (!longName) {
|
|
95
|
+
throw new Error("Invalid field found in subject name " + s[0]);
|
|
96
|
+
}
|
|
97
|
+
const value = s[1];
|
|
98
|
+
options[longName] = unquote(Buffer.from(value, "ascii").toString("utf8"));
|
|
99
|
+
});
|
|
100
|
+
return options as SubjectOptions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public toStringInternal(sep: string): string {
|
|
104
|
+
// https://reference.opcfoundation.org/v104/GDS/docs/7.6.4/
|
|
105
|
+
// The format of the subject name is a sequence of name value pairs separated by a ‘/’.
|
|
106
|
+
// The name shall be one of ‘CN’, ‘O’, ‘OU’, ‘DC’, ‘L’, ‘S’ or ‘C’ and
|
|
107
|
+
// shall be followed by a ‘=’ and then followed by the value.
|
|
108
|
+
// The value may be any printable character except for ‘”’.
|
|
109
|
+
// If the value contains a ‘/’ or a ‘=’ then it shall be enclosed in double quotes (‘”’).
|
|
110
|
+
|
|
111
|
+
const tmp: string[] = [];
|
|
112
|
+
if (this.country) {
|
|
113
|
+
tmp.push("C=" + enquoteIfNecessary(this.country));
|
|
114
|
+
}
|
|
115
|
+
if (this.state) {
|
|
116
|
+
tmp.push("ST=" + enquoteIfNecessary(this.state));
|
|
117
|
+
}
|
|
118
|
+
if (this.locality) {
|
|
119
|
+
tmp.push("L=" + enquoteIfNecessary(this.locality));
|
|
120
|
+
}
|
|
121
|
+
if (this.organization) {
|
|
122
|
+
tmp.push("O=" + enquoteIfNecessary(this.organization));
|
|
123
|
+
}
|
|
124
|
+
if (this.organizationalUnit) {
|
|
125
|
+
tmp.push("OU=" + enquoteIfNecessary(this.organizationalUnit));
|
|
126
|
+
}
|
|
127
|
+
if (this.commonName) {
|
|
128
|
+
tmp.push("CN=" + enquoteIfNecessary(this.commonName));
|
|
129
|
+
}
|
|
130
|
+
if (this.domainComponent) {
|
|
131
|
+
tmp.push("DC=" + enquoteIfNecessary(this.domainComponent));
|
|
132
|
+
}
|
|
133
|
+
return tmp.join(sep);
|
|
134
|
+
}
|
|
135
|
+
public toStringForOPCUA(): string {
|
|
136
|
+
return this.toStringInternal("/");
|
|
137
|
+
}
|
|
138
|
+
public toString(): string {
|
|
139
|
+
// standard for SSL is to have a / in front of each Field
|
|
140
|
+
// see https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm
|
|
141
|
+
const t = this.toStringForOPCUA();
|
|
142
|
+
return t ? "/" + t : t;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
// tslint:disable: no-console
|
|
2
|
-
|
|
3
|
-
// Now that we got a hash of the original certificate,
|
|
4
|
-
// we need to verify if we can obtain the same hash by using the same hashing function
|
|
5
|
-
// (in this case SHA-384). In order to do that, we need to extract just the body of
|
|
6
|
-
// the signed certificate. Which, in our case, is everything but the signature.
|
|
7
|
-
// The start of the body is always the first digit of the second line of the following command:
|
|
8
|
-
import * as crypto from "crypto";
|
|
9
|
-
|
|
10
|
-
import { Certificate, PrivateKey } from "./common";
|
|
11
|
-
import { split_der, exploreCertificate } from "./crypto_explore_certificate";
|
|
12
|
-
import { toPem } from "./crypto_utils";
|
|
13
|
-
import { _readAlgorithmIdentifier, _readSignatureValueBin, TagType, readTag, _readStruct, _getBlock } from "./asn1";
|
|
14
|
-
|
|
15
|
-
export function verifyCertificateOrClrSignature(certificateOrCrl: Buffer, parentCertificate: Certificate): boolean {
|
|
16
|
-
const block_info = readTag(certificateOrCrl, 0);
|
|
17
|
-
const blocks = _readStruct(certificateOrCrl, block_info);
|
|
18
|
-
const bufferToBeSigned = certificateOrCrl.slice(block_info.position, blocks[1].position - 2);
|
|
19
|
-
|
|
20
|
-
//xx console.log("bufferToBeSigned = ", bufferToBeSigned.length, bufferToBeSigned.toString("hex").substr(0, 50), bufferToBeSigned.toString("hex").substr(-10));
|
|
21
|
-
const signatureAlgorithm = _readAlgorithmIdentifier(certificateOrCrl, blocks[1]);
|
|
22
|
-
const signatureValue = _readSignatureValueBin(certificateOrCrl, blocks[2]);
|
|
23
|
-
|
|
24
|
-
const p = split_der(parentCertificate)[0];
|
|
25
|
-
//xx const publicKey = extractPublicKeyFromCertificateSync(p);
|
|
26
|
-
const certPem = toPem(p, "CERTIFICATE");
|
|
27
|
-
const verify = crypto.createVerify(signatureAlgorithm.identifier);
|
|
28
|
-
verify.update(bufferToBeSigned);
|
|
29
|
-
verify.end();
|
|
30
|
-
return verify.verify(certPem, signatureValue);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function verifyCertificateSignature(certificate: Certificate, parentCertificate: Certificate): boolean {
|
|
34
|
-
return verifyCertificateOrClrSignature(certificate, parentCertificate);
|
|
35
|
-
}
|
|
36
|
-
export function verifyCertificateRevocationListSignature(
|
|
37
|
-
certificateRevocationList: Certificate,
|
|
38
|
-
parentCertificate: Certificate
|
|
39
|
-
): boolean {
|
|
40
|
-
return verifyCertificateOrClrSignature(certificateRevocationList, parentCertificate);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type _VerifyStatus = "BadCertificateIssuerUseNotAllowed" | "BadCertificateInvalid" | "Good";
|
|
44
|
-
export async function verifyCertificateChain(certificateChain: Certificate[]): Promise<{ status: _VerifyStatus; reason: string }> {
|
|
45
|
-
// verify that all the certificate
|
|
46
|
-
// second certificate must be used for CertificateSign
|
|
47
|
-
|
|
48
|
-
for (let index = 1; index < certificateChain.length; index++) {
|
|
49
|
-
const cert = certificateChain[index - 1];
|
|
50
|
-
const certParent = certificateChain[index];
|
|
51
|
-
|
|
52
|
-
// parent child must have keyCertSign
|
|
53
|
-
const certParentInfo = exploreCertificate(certParent);
|
|
54
|
-
const keyUsage = certParentInfo.tbsCertificate.extensions!.keyUsage!;
|
|
55
|
-
|
|
56
|
-
// istanbul ignore next
|
|
57
|
-
if (!keyUsage.keyCertSign) {
|
|
58
|
-
return {
|
|
59
|
-
status: "BadCertificateIssuerUseNotAllowed",
|
|
60
|
-
reason: "One of the certificate in the chain has not keyUsage set for Certificate Signing",
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const parentSignChild = verifyCertificateSignature(cert, certParent);
|
|
65
|
-
if (!parentSignChild) {
|
|
66
|
-
return {
|
|
67
|
-
status: "BadCertificateInvalid",
|
|
68
|
-
reason: "One of the certificate in the chain is not signing the previous certificate",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const certInfo = exploreCertificate(cert);
|
|
72
|
-
|
|
73
|
-
// istanbul ignore next
|
|
74
|
-
if (!certInfo.tbsCertificate.extensions) {
|
|
75
|
-
return {
|
|
76
|
-
status: "BadCertificateInvalid",
|
|
77
|
-
reason: "Cannot find X409 Extension 3 in certificate",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// istanbul ignore next
|
|
82
|
-
if (!certParentInfo.tbsCertificate.extensions || !certInfo.tbsCertificate.extensions.authorityKeyIdentifier) {
|
|
83
|
-
return {
|
|
84
|
-
status: "BadCertificateInvalid",
|
|
85
|
-
reason: "Cannot find X409 Extension 3 in certificate (parent)",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// istanbul ignore next
|
|
90
|
-
if (
|
|
91
|
-
certParentInfo.tbsCertificate.extensions.subjectKeyIdentifier !==
|
|
92
|
-
certInfo.tbsCertificate.extensions.authorityKeyIdentifier.keyIdentifier
|
|
93
|
-
) {
|
|
94
|
-
return {
|
|
95
|
-
status: "BadCertificateInvalid",
|
|
96
|
-
reason:
|
|
97
|
-
"subjectKeyIdentifier authorityKeyIdentifier in child certificate do not match subjectKeyIdentifier of parent certificate",
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
status: "Good",
|
|
103
|
-
reason: `certificate chain is valid(length = ${certificateChain.length})`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
1
|
+
// tslint:disable: no-console
|
|
2
|
+
|
|
3
|
+
// Now that we got a hash of the original certificate,
|
|
4
|
+
// we need to verify if we can obtain the same hash by using the same hashing function
|
|
5
|
+
// (in this case SHA-384). In order to do that, we need to extract just the body of
|
|
6
|
+
// the signed certificate. Which, in our case, is everything but the signature.
|
|
7
|
+
// The start of the body is always the first digit of the second line of the following command:
|
|
8
|
+
import * as crypto from "crypto";
|
|
9
|
+
|
|
10
|
+
import { Certificate, PrivateKey } from "./common";
|
|
11
|
+
import { split_der, exploreCertificate } from "./crypto_explore_certificate";
|
|
12
|
+
import { toPem } from "./crypto_utils";
|
|
13
|
+
import { _readAlgorithmIdentifier, _readSignatureValueBin, TagType, readTag, _readStruct, _getBlock } from "./asn1";
|
|
14
|
+
|
|
15
|
+
export function verifyCertificateOrClrSignature(certificateOrCrl: Buffer, parentCertificate: Certificate): boolean {
|
|
16
|
+
const block_info = readTag(certificateOrCrl, 0);
|
|
17
|
+
const blocks = _readStruct(certificateOrCrl, block_info);
|
|
18
|
+
const bufferToBeSigned = certificateOrCrl.slice(block_info.position, blocks[1].position - 2);
|
|
19
|
+
|
|
20
|
+
//xx console.log("bufferToBeSigned = ", bufferToBeSigned.length, bufferToBeSigned.toString("hex").substr(0, 50), bufferToBeSigned.toString("hex").substr(-10));
|
|
21
|
+
const signatureAlgorithm = _readAlgorithmIdentifier(certificateOrCrl, blocks[1]);
|
|
22
|
+
const signatureValue = _readSignatureValueBin(certificateOrCrl, blocks[2]);
|
|
23
|
+
|
|
24
|
+
const p = split_der(parentCertificate)[0];
|
|
25
|
+
//xx const publicKey = extractPublicKeyFromCertificateSync(p);
|
|
26
|
+
const certPem = toPem(p, "CERTIFICATE");
|
|
27
|
+
const verify = crypto.createVerify(signatureAlgorithm.identifier);
|
|
28
|
+
verify.update(bufferToBeSigned);
|
|
29
|
+
verify.end();
|
|
30
|
+
return verify.verify(certPem, signatureValue);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function verifyCertificateSignature(certificate: Certificate, parentCertificate: Certificate): boolean {
|
|
34
|
+
return verifyCertificateOrClrSignature(certificate, parentCertificate);
|
|
35
|
+
}
|
|
36
|
+
export function verifyCertificateRevocationListSignature(
|
|
37
|
+
certificateRevocationList: Certificate,
|
|
38
|
+
parentCertificate: Certificate
|
|
39
|
+
): boolean {
|
|
40
|
+
return verifyCertificateOrClrSignature(certificateRevocationList, parentCertificate);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type _VerifyStatus = "BadCertificateIssuerUseNotAllowed" | "BadCertificateInvalid" | "Good";
|
|
44
|
+
export async function verifyCertificateChain(certificateChain: Certificate[]): Promise<{ status: _VerifyStatus; reason: string }> {
|
|
45
|
+
// verify that all the certificate
|
|
46
|
+
// second certificate must be used for CertificateSign
|
|
47
|
+
|
|
48
|
+
for (let index = 1; index < certificateChain.length; index++) {
|
|
49
|
+
const cert = certificateChain[index - 1];
|
|
50
|
+
const certParent = certificateChain[index];
|
|
51
|
+
|
|
52
|
+
// parent child must have keyCertSign
|
|
53
|
+
const certParentInfo = exploreCertificate(certParent);
|
|
54
|
+
const keyUsage = certParentInfo.tbsCertificate.extensions!.keyUsage!;
|
|
55
|
+
|
|
56
|
+
// istanbul ignore next
|
|
57
|
+
if (!keyUsage.keyCertSign) {
|
|
58
|
+
return {
|
|
59
|
+
status: "BadCertificateIssuerUseNotAllowed",
|
|
60
|
+
reason: "One of the certificate in the chain has not keyUsage set for Certificate Signing",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parentSignChild = verifyCertificateSignature(cert, certParent);
|
|
65
|
+
if (!parentSignChild) {
|
|
66
|
+
return {
|
|
67
|
+
status: "BadCertificateInvalid",
|
|
68
|
+
reason: "One of the certificate in the chain is not signing the previous certificate",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const certInfo = exploreCertificate(cert);
|
|
72
|
+
|
|
73
|
+
// istanbul ignore next
|
|
74
|
+
if (!certInfo.tbsCertificate.extensions) {
|
|
75
|
+
return {
|
|
76
|
+
status: "BadCertificateInvalid",
|
|
77
|
+
reason: "Cannot find X409 Extension 3 in certificate",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// istanbul ignore next
|
|
82
|
+
if (!certParentInfo.tbsCertificate.extensions || !certInfo.tbsCertificate.extensions.authorityKeyIdentifier) {
|
|
83
|
+
return {
|
|
84
|
+
status: "BadCertificateInvalid",
|
|
85
|
+
reason: "Cannot find X409 Extension 3 in certificate (parent)",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// istanbul ignore next
|
|
90
|
+
if (
|
|
91
|
+
certParentInfo.tbsCertificate.extensions.subjectKeyIdentifier !==
|
|
92
|
+
certInfo.tbsCertificate.extensions.authorityKeyIdentifier.keyIdentifier
|
|
93
|
+
) {
|
|
94
|
+
return {
|
|
95
|
+
status: "BadCertificateInvalid",
|
|
96
|
+
reason:
|
|
97
|
+
"subjectKeyIdentifier authorityKeyIdentifier in child certificate do not match subjectKeyIdentifier of parent certificate",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
status: "Good",
|
|
103
|
+
reason: `certificate chain is valid(length = ${certificateChain.length})`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cryptoProvider } from "@peculiar/x509";
|
|
2
|
+
|
|
3
|
+
// https://stackoverflow.com/questions/56807959/generate-public-key-from-private-key-using-webcrypto-api
|
|
4
|
+
export async function buildPublicKey(privateKey: CryptoKey): Promise<CryptoKey> {
|
|
5
|
+
const crypto = cryptoProvider.get();
|
|
6
|
+
|
|
7
|
+
// export private key to JWK
|
|
8
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
9
|
+
|
|
10
|
+
// remove private data from JWK
|
|
11
|
+
delete jwk.d;
|
|
12
|
+
delete jwk.dp;
|
|
13
|
+
delete jwk.dq;
|
|
14
|
+
delete jwk.q;
|
|
15
|
+
delete jwk.qi;
|
|
16
|
+
jwk.key_ops = ["encrypt", "wrapKey"];
|
|
17
|
+
|
|
18
|
+
// import public key
|
|
19
|
+
const publicKey = await crypto.subtle.importKey("jwk", jwk, { name: "RSA-OAEP", hash: "SHA-512" }, true, [
|
|
20
|
+
"encrypt",
|
|
21
|
+
"wrapKey",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
return publicKey;
|
|
25
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CertificatePurpose } from "../common";
|
|
2
|
+
import {x509} from "./_crypto";
|
|
3
|
+
|
|
4
|
+
// key usage of OPCUA Server or OPCUA Client
|
|
5
|
+
const keyUsageApplication =
|
|
6
|
+
x509.KeyUsageFlags.keyEncipherment | x509.KeyUsageFlags.dataEncipherment | x509.KeyUsageFlags.digitalSignature;
|
|
7
|
+
|
|
8
|
+
// key usage for CA certificate
|
|
9
|
+
const keyUsageCA = x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign;
|
|
10
|
+
|
|
11
|
+
export function getAttributes(purpose: CertificatePurpose): {
|
|
12
|
+
nsComment: string;
|
|
13
|
+
basicConstraints: x509.BasicConstraintsExtension;
|
|
14
|
+
keyUsageExtension: x509.ExtendedKeyUsage[];
|
|
15
|
+
usages: x509.KeyUsageFlags;
|
|
16
|
+
} {
|
|
17
|
+
let basicConstraints: x509.BasicConstraintsExtension;
|
|
18
|
+
let keyUsageExtension: x509.ExtendedKeyUsage[] = [];
|
|
19
|
+
let usages: x509.KeyUsageFlags;
|
|
20
|
+
let nsComment: string;
|
|
21
|
+
let extension: string;
|
|
22
|
+
switch (purpose) {
|
|
23
|
+
case CertificatePurpose.ForCertificateAuthority:
|
|
24
|
+
extension = "v3_ca";
|
|
25
|
+
/**
|
|
26
|
+
[ v3_ca ]
|
|
27
|
+
subjectKeyIdentifier = hash
|
|
28
|
+
authorityKeyIdentifier = keyid:always,issuer:always
|
|
29
|
+
* basicConstraints = CA:TRUE
|
|
30
|
+
* keyUsage = critical, cRLSign, keyCertSign
|
|
31
|
+
* nsComment = "Self-signed Certificate for CA generated by Node-OPCUA Certificate utility"
|
|
32
|
+
subjectAltName = $ENV::ALTNAME
|
|
33
|
+
*/
|
|
34
|
+
basicConstraints = new x509.BasicConstraintsExtension(true, undefined, false);
|
|
35
|
+
usages = keyUsageCA;
|
|
36
|
+
keyUsageExtension = [];
|
|
37
|
+
nsComment = "Self-signed certificate for CA generated by Node-OPCUA Certificate utility V2";
|
|
38
|
+
break;
|
|
39
|
+
case CertificatePurpose.ForApplication:
|
|
40
|
+
case CertificatePurpose.ForUserAuthentication:
|
|
41
|
+
default:
|
|
42
|
+
/**
|
|
43
|
+
[ v3_selfsigned]
|
|
44
|
+
subjectKeyIdentifier = hash
|
|
45
|
+
authorityKeyIdentifier = keyid,issuer
|
|
46
|
+
* basicConstraints = critical, CA:FALSE
|
|
47
|
+
* keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyCertSign
|
|
48
|
+
* extendedKeyUsage = clientAuth,serverAuth
|
|
49
|
+
* nsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility"
|
|
50
|
+
subjectAltName = $ENV::ALTNAME
|
|
51
|
+
*/
|
|
52
|
+
extension = "v3_selfsigned";
|
|
53
|
+
basicConstraints = new x509.BasicConstraintsExtension(false, undefined, true);
|
|
54
|
+
usages = keyUsageApplication;
|
|
55
|
+
keyUsageExtension = [x509.ExtendedKeyUsage.serverAuth, x509.ExtendedKeyUsage.clientAuth];
|
|
56
|
+
nsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility V2";
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return { nsComment, basicConstraints, keyUsageExtension, usages };
|
|
60
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Subject } from "../subject";
|
|
2
|
+
import { CertificatePurpose } from "../common";
|
|
3
|
+
import { getAttributes } from "./_get_attributes";
|
|
4
|
+
import { x509 } from "./_crypto";
|
|
5
|
+
import { buildPublicKey } from "./_build_public_key";
|
|
6
|
+
|
|
7
|
+
interface CreateCertificateSigningRequestOptions {
|
|
8
|
+
privateKey: CryptoKey;
|
|
9
|
+
notBefore?: Date;
|
|
10
|
+
notAfter?: Date;
|
|
11
|
+
validity?: number;
|
|
12
|
+
subject?: string;
|
|
13
|
+
dns?: string[];
|
|
14
|
+
ip?: string[];
|
|
15
|
+
applicationUri?: string;
|
|
16
|
+
purpose: CertificatePurpose;
|
|
17
|
+
}
|
|
18
|
+
export async function createCertificateSigningRequest({
|
|
19
|
+
privateKey,
|
|
20
|
+
subject,
|
|
21
|
+
dns,
|
|
22
|
+
ip,
|
|
23
|
+
applicationUri,
|
|
24
|
+
purpose,
|
|
25
|
+
}: CreateCertificateSigningRequestOptions) {
|
|
26
|
+
const modulusLength = 2048;
|
|
27
|
+
|
|
28
|
+
const alg = {
|
|
29
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
30
|
+
hash: { name: "SHA-256" },
|
|
31
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
32
|
+
modulusLength,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const publicKey = await buildPublicKey(privateKey);
|
|
36
|
+
|
|
37
|
+
const keys = {
|
|
38
|
+
privateKey,
|
|
39
|
+
publicKey,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const alternativeNameExtensions: x509.JsonGeneralName[] = [];
|
|
43
|
+
dns && dns.forEach((d) => alternativeNameExtensions.push({ type: "dns", value: d }));
|
|
44
|
+
ip && ip.forEach((d) => alternativeNameExtensions.push({ type: "ip", value: d }));
|
|
45
|
+
applicationUri && alternativeNameExtensions.push({ type: "url", value: applicationUri });
|
|
46
|
+
|
|
47
|
+
const { basicConstraints, usages } = getAttributes(purpose);
|
|
48
|
+
|
|
49
|
+
const s = new Subject(subject || "");
|
|
50
|
+
const s1 = s.toStringInternal(", ");
|
|
51
|
+
const name = s1;
|
|
52
|
+
|
|
53
|
+
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
|
|
54
|
+
name,
|
|
55
|
+
keys,
|
|
56
|
+
signingAlgorithm: alg,
|
|
57
|
+
extensions: [
|
|
58
|
+
basicConstraints,
|
|
59
|
+
new x509.KeyUsagesExtension(usages, true),
|
|
60
|
+
new x509.SubjectAlternativeNameExtension(alternativeNameExtensions),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
return { csr: csr.toString("pem"), der: csr };
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as x509 from "@peculiar/x509";
|
|
2
|
+
import { Crypto } from "@peculiar/webcrypto";
|
|
3
|
+
const crypto = new Crypto();
|
|
4
|
+
x509.cryptoProvider.set(crypto);
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
interface KeyAlgorithm {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
type KeyType = "private" | "public" | "secret";
|
|
12
|
+
type KeyUsage = "decrypt" | "deriveBits" | "deriveKey" | "encrypt" | "sign" | "unwrapKey" | "verify" | "wrapKey";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The CryptoKey dictionary of the Web Crypto API represents a cryptographic key.
|
|
16
|
+
* Available only in secure contexts.
|
|
17
|
+
*/
|
|
18
|
+
interface CryptoKey {
|
|
19
|
+
readonly algorithm: KeyAlgorithm;
|
|
20
|
+
readonly extractable: boolean;
|
|
21
|
+
readonly type: KeyType;
|
|
22
|
+
readonly usages: KeyUsage[];
|
|
23
|
+
}
|
|
24
|
+
interface CryptoKeyPair {
|
|
25
|
+
privateKey: CryptoKey;
|
|
26
|
+
publicKey: CryptoKey;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export async function generateKeyPair(modulusLength: 1024 | 2048 | 3072 | 4096 = 2048): Promise<CryptoKeyPair> {
|
|
32
|
+
const alg: RsaHashedKeyGenParams = {
|
|
33
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
34
|
+
hash: { name: "SHA-256" },
|
|
35
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
36
|
+
modulusLength,
|
|
37
|
+
};
|
|
38
|
+
const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
|
39
|
+
|
|
40
|
+
return keys;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function generatePrivateKey(modulusLength: 1024 | 2048 | 3072 | 4096 = 2048): Promise<CryptoKey> {
|
|
44
|
+
return (await generateKeyPair(modulusLength)).privateKey;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function privateKeyToPEM(privateKey: CryptoKey) {
|
|
48
|
+
const privDer = await crypto.subtle.exportKey("pkcs8", privateKey);
|
|
49
|
+
const privPem = x509.PemConverter.encode(privDer, "PRIVATE KEY");
|
|
50
|
+
return { privPem, privDer };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function derToPrivateKey(privDer: ArrayBuffer): Promise<CryptoKey> {
|
|
54
|
+
return await crypto.subtle.importKey(
|
|
55
|
+
"pkcs8",
|
|
56
|
+
privDer,
|
|
57
|
+
{
|
|
58
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
59
|
+
hash: { name: "SHA-256" },
|
|
60
|
+
},
|
|
61
|
+
true,
|
|
62
|
+
["sign", "encrypt", "decrypt", "verify", "wrapKey", "unwrapKey", "deriveKey", "deriveBits"]
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function pemToPrivateKey(pem: string): Promise<CryptoKey> {
|
|
67
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
|
|
68
|
+
const privDer = x509.PemConverter.decode(pem);
|
|
69
|
+
return derToPrivateKey(privDer[0]);
|
|
70
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Subject } from "../subject";
|
|
2
|
+
import { CertificatePurpose } from "../common";
|
|
3
|
+
import { x509 } from "./_crypto";
|
|
4
|
+
import { getAttributes } from "./_get_attributes";
|
|
5
|
+
import { buildPublicKey } from "./_build_public_key";
|
|
6
|
+
|
|
7
|
+
export interface CreateSelfSignCertificateOptions {
|
|
8
|
+
privateKey: CryptoKey;
|
|
9
|
+
notBefore?: Date;
|
|
10
|
+
notAfter?: Date;
|
|
11
|
+
validity?: number;
|
|
12
|
+
// CN=common/O=Org/C=US/ST=State/L=City
|
|
13
|
+
subject?: string;
|
|
14
|
+
dns?: string[];
|
|
15
|
+
ip?: string[];
|
|
16
|
+
applicationUri?: string;
|
|
17
|
+
purpose: CertificatePurpose;
|
|
18
|
+
}
|
|
19
|
+
export async function createSelfSignedCertificate({
|
|
20
|
+
privateKey,
|
|
21
|
+
notAfter,
|
|
22
|
+
notBefore,
|
|
23
|
+
validity,
|
|
24
|
+
subject,
|
|
25
|
+
dns,
|
|
26
|
+
ip,
|
|
27
|
+
applicationUri,
|
|
28
|
+
purpose,
|
|
29
|
+
}: CreateSelfSignCertificateOptions) {
|
|
30
|
+
|
|
31
|
+
const publicKey = await buildPublicKey(privateKey);
|
|
32
|
+
|
|
33
|
+
const keys = {
|
|
34
|
+
privateKey,
|
|
35
|
+
publicKey,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const { nsComment, basicConstraints, keyUsageExtension, usages } = getAttributes(purpose);
|
|
39
|
+
|
|
40
|
+
notBefore = notBefore || new Date();
|
|
41
|
+
validity = validity || 0;
|
|
42
|
+
if (!notAfter) {
|
|
43
|
+
validity = validity || 365;
|
|
44
|
+
}
|
|
45
|
+
notAfter = notAfter || new Date(notBefore.getTime() + validity * 24 * 60 * 60 * 1000);
|
|
46
|
+
|
|
47
|
+
const alternativeNameExtensions: x509.JsonGeneralName[] = [];
|
|
48
|
+
dns && dns.forEach((d) => alternativeNameExtensions.push({ type: "dns", value: d }));
|
|
49
|
+
ip && ip.forEach((d) => alternativeNameExtensions.push({ type: "ip", value: d }));
|
|
50
|
+
applicationUri && alternativeNameExtensions.push({ type: "url", value: applicationUri });
|
|
51
|
+
|
|
52
|
+
// https://opensource.apple.com/source/OpenSSH/OpenSSH-186/osslshim/heimdal-asn1/rfc2459.asn1.auto.html
|
|
53
|
+
const ID_NETSCAPE_COMMENT = "2.16.840.1.113730.1.13";
|
|
54
|
+
|
|
55
|
+
const s = new Subject(subject || "");
|
|
56
|
+
const s1 = s.toStringInternal(", ");
|
|
57
|
+
const name = s1;
|
|
58
|
+
// const issuer = s1;
|
|
59
|
+
/**
|
|
60
|
+
* name: "CN=Test, O=Дом",
|
|
61
|
+
* subject: "CN=Test, O=Дом",
|
|
62
|
+
* issuer: "CN=Test, O=Дом",
|
|
63
|
+
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
// const gg = new x509.GeneralNames(gga);
|
|
67
|
+
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
|
68
|
+
serialNumber: "01",
|
|
69
|
+
name,
|
|
70
|
+
// subject: s1,
|
|
71
|
+
// issuer,
|
|
72
|
+
notBefore,
|
|
73
|
+
notAfter,
|
|
74
|
+
|
|
75
|
+
signingAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
|
|
76
|
+
|
|
77
|
+
keys,
|
|
78
|
+
|
|
79
|
+
extensions: [
|
|
80
|
+
new x509.Extension(ID_NETSCAPE_COMMENT, false, Buffer.from(nsComment, "ascii")),
|
|
81
|
+
// new x509.BasicConstraintsExtension(true, 2, true),
|
|
82
|
+
basicConstraints,
|
|
83
|
+
new x509.ExtendedKeyUsageExtension(keyUsageExtension, true),
|
|
84
|
+
new x509.KeyUsagesExtension(usages, true),
|
|
85
|
+
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
|
|
86
|
+
new x509.SubjectAlternativeNameExtension(alternativeNameExtensions),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { cert: cert.toString("pem"), der: cert };
|
|
91
|
+
}
|