@sphereon/ssi-sdk-ext.x509-utils 0.26.1-next.3 → 0.26.1-next.31
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/dist/x509/crypto.d.ts +2 -0
- package/dist/x509/crypto.d.ts.map +1 -0
- package/dist/x509/crypto.js +28 -0
- package/dist/x509/crypto.js.map +1 -0
- package/dist/x509/rsa-key.d.ts.map +1 -1
- package/dist/x509/rsa-key.js +4 -3
- package/dist/x509/rsa-key.js.map +1 -1
- package/dist/x509/rsa-signer.d.ts.map +1 -1
- package/dist/x509/rsa-signer.js +3 -2
- package/dist/x509/rsa-signer.js.map +1 -1
- package/dist/x509/x509-utils.d.ts +2 -1
- package/dist/x509/x509-utils.d.ts.map +1 -1
- package/dist/x509/x509-utils.js +10 -3
- package/dist/x509/x509-utils.js.map +1 -1
- package/dist/x509/x509-validator.d.ts +19 -8
- package/dist/x509/x509-validator.d.ts.map +1 -1
- package/dist/x509/x509-validator.js +263 -63
- package/dist/x509/x509-validator.js.map +1 -1
- package/package.json +7 -2
- package/src/x509/crypto.ts +19 -0
- package/src/x509/rsa-key.ts +4 -3
- package/src/x509/rsa-signer.ts +3 -2
- package/src/x509/x509-utils.ts +11 -5
- package/src/x509/x509-validator.ts +354 -94
|
@@ -1,16 +1,14 @@
|
|
|
1
|
+
import { AsnParser } from '@peculiar/asn1-schema'
|
|
2
|
+
import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509'
|
|
3
|
+
import { AlgorithmProvider, X509Certificate } from '@peculiar/x509'
|
|
4
|
+
// import {calculateJwkThumbprint} from "@sphereon/ssi-sdk-ext.key-utils";
|
|
5
|
+
import { JWK } from '@sphereon/ssi-types'
|
|
1
6
|
import x509 from 'js-x509-utils'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
AttributeTypeAndValue,
|
|
5
|
-
Certificate,
|
|
6
|
-
CertificateChainValidationEngine,
|
|
7
|
-
CryptoEngine,
|
|
8
|
-
getCrypto,
|
|
9
|
-
id_SubjectAltName,
|
|
10
|
-
setEngine,
|
|
11
|
-
} from 'pkijs'
|
|
7
|
+
import { AltName, AttributeTypeAndValue, Certificate, CryptoEngine, getCrypto, id_SubjectAltName, setEngine } from 'pkijs'
|
|
8
|
+
import { container } from 'tsyringe'
|
|
12
9
|
import * as u8a from 'uint8arrays'
|
|
13
|
-
import {
|
|
10
|
+
import { globalCrypto } from './crypto'
|
|
11
|
+
import { areCertificatesEqual, derToPEM, pemOrDerToX509Certificate } from './x509-utils'
|
|
14
12
|
|
|
15
13
|
export type DNInfo = {
|
|
16
14
|
DN: string
|
|
@@ -35,8 +33,10 @@ export type X509ValidationResult = {
|
|
|
35
33
|
error: boolean
|
|
36
34
|
critical: boolean
|
|
37
35
|
message: string
|
|
36
|
+
detailMessage?: string
|
|
38
37
|
verificationTime: Date
|
|
39
38
|
certificateChain?: Array<CertificateInfo>
|
|
39
|
+
trustAnchor?: CertificateInfo
|
|
40
40
|
client?: {
|
|
41
41
|
// In case client id and scheme were passed in we return them for easy access. It means they are validated
|
|
42
42
|
clientId: string
|
|
@@ -45,23 +45,9 @@ export type X509ValidationResult = {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const defaultCryptoEngine = () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if ('webkitSubtle' in self.crypto) {
|
|
52
|
-
engineName = 'safari'
|
|
53
|
-
}
|
|
54
|
-
setEngine(engineName, new CryptoEngine({ name: engineName, crypto: crypto }))
|
|
55
|
-
}
|
|
56
|
-
} else if (typeof crypto !== 'undefined' && 'webcrypto' in crypto) {
|
|
57
|
-
const name = 'NodeJS ^15'
|
|
58
|
-
const nodeCrypto = crypto.webcrypto
|
|
59
|
-
// @ts-ignore
|
|
60
|
-
setEngine(name, new CryptoEngine({ name, crypto: nodeCrypto }))
|
|
61
|
-
} else if (typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined') {
|
|
62
|
-
const name = 'crypto'
|
|
63
|
-
setEngine(name, new CryptoEngine({ name, crypto: crypto }))
|
|
64
|
-
}
|
|
48
|
+
const name = 'crypto'
|
|
49
|
+
setEngine(name, new CryptoEngine({ name, crypto: globalCrypto(false) }))
|
|
50
|
+
return getCrypto(true)
|
|
65
51
|
}
|
|
66
52
|
|
|
67
53
|
export const getCertificateInfo = async (
|
|
@@ -70,14 +56,17 @@ export const getCertificateInfo = async (
|
|
|
70
56
|
sanTypeFilter: SubjectAlternativeGeneralName | SubjectAlternativeGeneralName[]
|
|
71
57
|
}
|
|
72
58
|
): Promise<CertificateInfo> => {
|
|
73
|
-
|
|
59
|
+
let publicKeyJWK: JWK | undefined
|
|
60
|
+
try {
|
|
61
|
+
publicKeyJWK = (await getCertificateSubjectPublicKeyJWK(certificate)) as JWK
|
|
62
|
+
} catch (e) {}
|
|
74
63
|
return {
|
|
75
64
|
issuer: { dn: getIssuerDN(certificate) },
|
|
76
65
|
subject: {
|
|
77
66
|
dn: getSubjectDN(certificate),
|
|
78
67
|
subjectAlternativeNames: getSubjectAlternativeNames(certificate, { typeFilter: opts?.sanTypeFilter }),
|
|
79
68
|
},
|
|
80
|
-
publicKeyJWK
|
|
69
|
+
publicKeyJWK,
|
|
81
70
|
notBefore: certificate.notBefore.value,
|
|
82
71
|
notAfter: certificate.notAfter.value,
|
|
83
72
|
// certificate
|
|
@@ -85,6 +74,9 @@ export const getCertificateInfo = async (
|
|
|
85
74
|
}
|
|
86
75
|
|
|
87
76
|
export type X509CertificateChainValidationOpts = {
|
|
77
|
+
// If no trust anchor is found, but the chain itself checks out, allow. (defaults to false:)
|
|
78
|
+
allowNoTrustAnchorsFound?: boolean
|
|
79
|
+
|
|
88
80
|
// Trust the supplied root from the chain, when no anchors are being passed in.
|
|
89
81
|
trustRootWhenNoAnchors?: boolean
|
|
90
82
|
// Do not perform a chain validation check if the chain only has a single value. This means only the certificate itself will be validated. No chain checks for CA certs will be performed. Only used when the cert has no issuer
|
|
@@ -93,6 +85,8 @@ export type X509CertificateChainValidationOpts = {
|
|
|
93
85
|
// Similar to regular trust anchors, but no validation is performed whatsoever. Do not use in production settings! Can be handy with self generated certificates as we perform many validations, making it hard to test with self-signed certs. Only applied in case a chain with 1 element is passed in to really make sure people do not abuse this option
|
|
94
86
|
blindlyTrustedAnchors?: string[]
|
|
95
87
|
|
|
88
|
+
disallowReversedChain?: boolean
|
|
89
|
+
|
|
96
90
|
client?: {
|
|
97
91
|
// If provided both are required. Validates the leaf certificate against the clientId and scheme
|
|
98
92
|
clientId: string
|
|
@@ -100,21 +94,17 @@ export type X509CertificateChainValidationOpts = {
|
|
|
100
94
|
}
|
|
101
95
|
}
|
|
102
96
|
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
* @param pemOrDerChain The order must be that the Certs signing another cert must come one after another. So first the signing cert, then any cert signing that cert and so on
|
|
106
|
-
* @param trustedPEMs
|
|
107
|
-
* @param verificationTime
|
|
108
|
-
* @param opts
|
|
109
|
-
*/
|
|
110
97
|
export const validateX509CertificateChain = async ({
|
|
111
98
|
chain: pemOrDerChain,
|
|
112
99
|
trustAnchors,
|
|
113
100
|
verificationTime = new Date(),
|
|
114
101
|
opts = {
|
|
102
|
+
// If no trust anchor is found, but the chain itself checks out, allow. (defaults to false:)
|
|
103
|
+
allowNoTrustAnchorsFound: false,
|
|
115
104
|
trustRootWhenNoAnchors: false,
|
|
116
105
|
allowSingleNoCAChainElement: true,
|
|
117
106
|
blindlyTrustedAnchors: [],
|
|
107
|
+
disallowReversedChain: false,
|
|
118
108
|
},
|
|
119
109
|
}: {
|
|
120
110
|
chain: (Uint8Array | string)[]
|
|
@@ -122,7 +112,37 @@ export const validateX509CertificateChain = async ({
|
|
|
122
112
|
verificationTime?: Date
|
|
123
113
|
opts?: X509CertificateChainValidationOpts
|
|
124
114
|
}): Promise<X509ValidationResult> => {
|
|
125
|
-
|
|
115
|
+
// We allow 1 reversal. We reverse by default as the implementation expects the root ca first, whilst x5c is the opposite. Reversed becomes true if the impl reverses the chain
|
|
116
|
+
return await validateX509CertificateChainImpl({
|
|
117
|
+
reversed: false,
|
|
118
|
+
chain: [...pemOrDerChain].reverse(),
|
|
119
|
+
trustAnchors,
|
|
120
|
+
verificationTime,
|
|
121
|
+
opts,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
const validateX509CertificateChainImpl = async ({
|
|
125
|
+
reversed,
|
|
126
|
+
chain: pemOrDerChain,
|
|
127
|
+
trustAnchors,
|
|
128
|
+
verificationTime: verifyAt,
|
|
129
|
+
opts,
|
|
130
|
+
}: {
|
|
131
|
+
reversed: boolean
|
|
132
|
+
chain: (Uint8Array | string)[]
|
|
133
|
+
trustAnchors?: string[]
|
|
134
|
+
verificationTime: Date | string // string for REST API
|
|
135
|
+
opts: X509CertificateChainValidationOpts
|
|
136
|
+
}): Promise<X509ValidationResult> => {
|
|
137
|
+
const verificationTime: Date = typeof verifyAt === 'string' ? new Date(verifyAt) : verifyAt
|
|
138
|
+
const {
|
|
139
|
+
allowNoTrustAnchorsFound = false,
|
|
140
|
+
trustRootWhenNoAnchors = false,
|
|
141
|
+
allowSingleNoCAChainElement = true,
|
|
142
|
+
blindlyTrustedAnchors = [],
|
|
143
|
+
disallowReversedChain = false,
|
|
144
|
+
client,
|
|
145
|
+
} = opts
|
|
126
146
|
const trustedPEMs = trustRootWhenNoAnchors && !trustAnchors ? [pemOrDerChain[pemOrDerChain.length - 1]] : trustAnchors
|
|
127
147
|
|
|
128
148
|
if (pemOrDerChain.length === 0) {
|
|
@@ -133,94 +153,323 @@ export const validateX509CertificateChain = async ({
|
|
|
133
153
|
verificationTime,
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
|
-
|
|
137
|
-
const certs = pemOrDerChain.map(pemOrDerToX509Certificate)
|
|
138
|
-
const trustedCerts = trustedPEMs ? trustedPEMs.map(pemOrDerToX509Certificate) : undefined
|
|
139
156
|
defaultCryptoEngine()
|
|
140
157
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
158
|
+
// x5c always starts with the leaf cert at index 0 and then the cas. Our internal pkijs service expects it the other way around. Before calling this function the change has been revered
|
|
159
|
+
const chain = await Promise.all(pemOrDerChain.map((raw) => parseCertificate(raw)))
|
|
160
|
+
const x5cOrdereredChain = reversed ? [...chain] : [...chain].reverse()
|
|
161
|
+
|
|
162
|
+
const trustedCerts = trustedPEMs ? await Promise.all(trustedPEMs.map((raw) => parseCertificate(raw))) : undefined
|
|
163
|
+
const blindlyTrusted =
|
|
164
|
+
(
|
|
165
|
+
await Promise.all(
|
|
166
|
+
blindlyTrustedAnchors.map((raw) => {
|
|
167
|
+
try {
|
|
168
|
+
return parseCertificate(raw)
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// @ts-ignore
|
|
171
|
+
console.log(`Failed to parse blindly trusted certificate ${raw}. Error: ${e.message}`)
|
|
172
|
+
return undefined
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
).filter((cert): cert is ParsedCertificate => cert !== undefined) ?? []
|
|
177
|
+
const leafCert = x5cOrdereredChain[0]
|
|
178
|
+
|
|
179
|
+
const chainLength = chain.length
|
|
180
|
+
var foundTrustAnchor: ParsedCertificate | undefined = undefined
|
|
181
|
+
for (let i = 0; i < chainLength; i++) {
|
|
182
|
+
const currentCert = chain[i]
|
|
183
|
+
const previousCert = i > 0 ? chain[i - 1] : undefined
|
|
184
|
+
const blindlyTrustedCert = blindlyTrusted.find((trusted) => areCertificatesEqual(trusted.certificate, currentCert.certificate))
|
|
185
|
+
if (blindlyTrustedCert) {
|
|
151
186
|
console.log(`Certificate chain validation success as single cert if blindly trusted. WARNING: ONLY USE FOR TESTING PURPOSES.`)
|
|
152
187
|
return {
|
|
153
188
|
error: false,
|
|
154
|
-
critical:
|
|
189
|
+
critical: false,
|
|
155
190
|
message: `Certificate chain validation success as single cert if blindly trusted. WARNING: ONLY USE FOR TESTING PURPOSES.`,
|
|
191
|
+
detailMessage: `Blindly trusted certificate ${blindlyTrustedCert.certificateInfo.subject.dn.DN} was found in the chain.`,
|
|
192
|
+
trustAnchor: blindlyTrustedCert?.certificateInfo,
|
|
156
193
|
verificationTime,
|
|
157
|
-
certificateChain:
|
|
194
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
158
195
|
...(client && { client }),
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
198
|
+
if (previousCert) {
|
|
199
|
+
if (currentCert.x509Certificate.issuer !== previousCert.x509Certificate.subject) {
|
|
200
|
+
if (!reversed && !disallowReversedChain) {
|
|
201
|
+
return await validateX509CertificateChainImpl({
|
|
202
|
+
reversed: true,
|
|
203
|
+
chain: [...pemOrDerChain].reverse(),
|
|
204
|
+
opts,
|
|
205
|
+
verificationTime,
|
|
206
|
+
trustAnchors,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
165
209
|
return {
|
|
166
|
-
error:
|
|
210
|
+
error: true,
|
|
167
211
|
critical: true,
|
|
168
|
-
|
|
212
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
213
|
+
message: `Certificate chain validation failed for ${leafCert.certificateInfo.subject.dn.DN}.`,
|
|
214
|
+
detailMessage: `The certificate ${currentCert.certificateInfo.subject.dn.DN} with issuer ${currentCert.x509Certificate.issuer}, is not signed by the previous certificate ${previousCert?.certificateInfo.subject.dn.DN} with subject string ${previousCert?.x509Certificate.subject}.`,
|
|
169
215
|
verificationTime,
|
|
170
|
-
certificateChain: [await getCertificateInfo(cert)],
|
|
171
216
|
...(client && { client }),
|
|
172
217
|
}
|
|
173
218
|
}
|
|
174
219
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
220
|
+
const result = await currentCert.x509Certificate.verify(
|
|
221
|
+
{
|
|
222
|
+
date: verificationTime,
|
|
223
|
+
publicKey: previousCert?.x509Certificate?.publicKey,
|
|
224
|
+
},
|
|
225
|
+
getCrypto()?.crypto ?? crypto ?? global.crypto
|
|
226
|
+
)
|
|
227
|
+
if (!result) {
|
|
228
|
+
// First cert needs to be self signed
|
|
229
|
+
if (i == 0 && !reversed && !disallowReversedChain) {
|
|
230
|
+
return await validateX509CertificateChainImpl({
|
|
231
|
+
reversed: true,
|
|
232
|
+
chain: [...pemOrDerChain].reverse(),
|
|
233
|
+
opts,
|
|
234
|
+
verificationTime,
|
|
235
|
+
trustAnchors,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
182
238
|
|
|
183
|
-
try {
|
|
184
|
-
const verification = await validationEngine.verify()
|
|
185
|
-
if (!verification.result || !verification.certificatePath) {
|
|
186
239
|
return {
|
|
187
240
|
error: true,
|
|
188
241
|
critical: true,
|
|
189
|
-
message:
|
|
242
|
+
message: `Certificate chain validation failed for ${leafCert.certificateInfo.subject.dn.DN}.`,
|
|
243
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
244
|
+
detailMessage: `Verification of the certificate ${currentCert.certificateInfo.subject.dn.DN} with issuer ${
|
|
245
|
+
currentCert.x509Certificate.issuer
|
|
246
|
+
} failed. Public key: ${JSON.stringify(currentCert.certificateInfo.publicKeyJWK)}.`,
|
|
190
247
|
verificationTime,
|
|
191
248
|
...(client && { client }),
|
|
192
249
|
}
|
|
193
250
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
251
|
+
|
|
252
|
+
foundTrustAnchor = foundTrustAnchor ?? trustedCerts?.find((trusted) => isSameCertificate(trusted.x509Certificate, currentCert.x509Certificate))
|
|
253
|
+
|
|
254
|
+
if (i === 0 && chainLength === 1 && allowSingleNoCAChainElement) {
|
|
255
|
+
return {
|
|
256
|
+
error: false,
|
|
257
|
+
critical: false,
|
|
258
|
+
message: `Certificate chain succeeded as allow single cert result is allowed: ${leafCert.certificateInfo.subject.dn.DN}.`,
|
|
259
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
260
|
+
trustAnchor: foundTrustAnchor?.certificateInfo,
|
|
261
|
+
verificationTime,
|
|
262
|
+
...(client && { client }),
|
|
199
263
|
}
|
|
200
264
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
})
|
|
205
|
-
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (foundTrustAnchor?.certificateInfo || allowNoTrustAnchorsFound) {
|
|
206
268
|
return {
|
|
207
269
|
error: false,
|
|
208
270
|
critical: false,
|
|
209
271
|
message: `Certificate chain was valid`,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
error: true,
|
|
217
|
-
critical: true,
|
|
218
|
-
message: `Certificate chain was invalid, ${error.message ?? '<unknown error>'}`,
|
|
272
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
273
|
+
detailMessage: foundTrustAnchor
|
|
274
|
+
? `The leaf certificate ${leafCert.certificateInfo.subject.dn.DN} is part of a chain with trust anchor ${foundTrustAnchor?.certificateInfo.subject.dn.DN}.`
|
|
275
|
+
: `The leaf certificate ${leafCert.certificateInfo.subject.dn.DN} and chain were valid, but no trust anchor has been found. Ignoring as user allowed (allowNoTrustAnchorsFound: ${allowNoTrustAnchorsFound}).)`,
|
|
276
|
+
trustAnchor: foundTrustAnchor?.certificateInfo,
|
|
219
277
|
verificationTime,
|
|
220
278
|
...(client && { client }),
|
|
221
279
|
}
|
|
222
280
|
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
error: true,
|
|
284
|
+
critical: true,
|
|
285
|
+
message: `Certificate chain validation failed for ${leafCert.certificateInfo.subject.dn.DN}.`,
|
|
286
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
287
|
+
detailMessage: `No trust anchor was found in the chain. between (intermediate) CA ${
|
|
288
|
+
x5cOrdereredChain[chain.length - 1].certificateInfo.subject.dn.DN
|
|
289
|
+
} and leaf ${x5cOrdereredChain[0].certificateInfo.subject.dn.DN}.`,
|
|
290
|
+
verificationTime,
|
|
291
|
+
...(client && { client }),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const isSameCertificate = (cert1: X509Certificate, cert2: X509Certificate): boolean => {
|
|
296
|
+
return cert1.rawData.toString() === cert2.rawData.toString()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const algorithmProvider: AlgorithmProvider = container.resolve(AlgorithmProvider)
|
|
300
|
+
export const getX509AlgorithmProvider = (): AlgorithmProvider => {
|
|
301
|
+
return algorithmProvider
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export type ParsedCertificate = {
|
|
305
|
+
publicKeyInfo: SubjectPublicKeyInfo
|
|
306
|
+
publicKeyJwk?: JWK
|
|
307
|
+
publicKeyRaw: Uint8Array
|
|
308
|
+
publicKeyAlgorithm: Algorithm
|
|
309
|
+
certificateInfo: CertificateInfo
|
|
310
|
+
certificate: Certificate
|
|
311
|
+
x509Certificate: X509Certificate
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const parseCertificate = async (rawCert: string | Uint8Array): Promise<ParsedCertificate> => {
|
|
315
|
+
const x509Certificate = new X509Certificate(rawCert)
|
|
316
|
+
const publicKeyInfo = AsnParser.parse(x509Certificate.publicKey.rawData, SubjectPublicKeyInfo)
|
|
317
|
+
const publicKeyRaw = new Uint8Array(publicKeyInfo.subjectPublicKey)
|
|
318
|
+
let publicKeyJwk: JWK | undefined = undefined
|
|
319
|
+
try {
|
|
320
|
+
publicKeyJwk = (await getCertificateSubjectPublicKeyJWK(new Uint8Array(x509Certificate.rawData))) as JWK
|
|
321
|
+
} catch (e: any) {
|
|
322
|
+
console.error(e.message)
|
|
323
|
+
}
|
|
324
|
+
const certificate = pemOrDerToX509Certificate(rawCert)
|
|
325
|
+
const certificateInfo = await getCertificateInfo(certificate)
|
|
326
|
+
const publicKeyAlgorithm = getX509AlgorithmProvider().toWebAlgorithm(publicKeyInfo.algorithm)
|
|
327
|
+
return {
|
|
328
|
+
publicKeyAlgorithm,
|
|
329
|
+
publicKeyInfo,
|
|
330
|
+
publicKeyJwk,
|
|
331
|
+
publicKeyRaw,
|
|
332
|
+
certificateInfo,
|
|
333
|
+
certificate,
|
|
334
|
+
x509Certificate,
|
|
335
|
+
}
|
|
223
336
|
}
|
|
337
|
+
/*
|
|
338
|
+
|
|
339
|
+
/!**
|
|
340
|
+
*
|
|
341
|
+
* @param pemOrDerChain The order must be that the Certs signing another cert must come one after another. So first the signing cert, then any cert signing that cert and so on
|
|
342
|
+
* @param trustedPEMs
|
|
343
|
+
* @param verificationTime
|
|
344
|
+
* @param opts
|
|
345
|
+
*!/
|
|
346
|
+
export const validateX509CertificateChainOrg = async ({
|
|
347
|
+
chain: pemOrDerChain,
|
|
348
|
+
trustAnchors,
|
|
349
|
+
verificationTime = new Date(),
|
|
350
|
+
opts = {
|
|
351
|
+
trustRootWhenNoAnchors: false,
|
|
352
|
+
allowSingleNoCAChainElement: true,
|
|
353
|
+
blindlyTrustedAnchors: [],
|
|
354
|
+
},
|
|
355
|
+
}: {
|
|
356
|
+
chain: (Uint8Array | string)[]
|
|
357
|
+
trustAnchors?: string[]
|
|
358
|
+
verificationTime?: Date
|
|
359
|
+
opts?: X509CertificateChainValidationOpts
|
|
360
|
+
}): Promise<X509ValidationResult> => {
|
|
361
|
+
const {
|
|
362
|
+
trustRootWhenNoAnchors = false,
|
|
363
|
+
allowSingleNoCAChainElement = true,
|
|
364
|
+
blindlyTrustedAnchors = [],
|
|
365
|
+
client
|
|
366
|
+
} = opts
|
|
367
|
+
const trustedPEMs = trustRootWhenNoAnchors && !trustAnchors ? [pemOrDerChain[pemOrDerChain.length - 1]] : trustAnchors
|
|
368
|
+
|
|
369
|
+
if (pemOrDerChain.length === 0) {
|
|
370
|
+
return {
|
|
371
|
+
error: true,
|
|
372
|
+
critical: true,
|
|
373
|
+
message: 'Certificate chain in DER or PEM format must not be empty',
|
|
374
|
+
verificationTime,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// x5c always starts with the leaf cert at index 0 and then the cas. Our internal pkijs service expects it the other way around
|
|
379
|
+
const certs = pemOrDerChain.map(pemOrDerToX509Certificate).reverse()
|
|
380
|
+
const trustedCerts = trustedPEMs ? trustedPEMs.map(pemOrDerToX509Certificate) : undefined
|
|
381
|
+
defaultCryptoEngine()
|
|
382
|
+
|
|
383
|
+
if (pemOrDerChain.length === 1) {
|
|
384
|
+
const singleCert = typeof pemOrDerChain[0] === 'string' ? pemOrDerChain[0] : u8a.toString(pemOrDerChain[0], 'base64pad')
|
|
385
|
+
const cert = pemOrDerToX509Certificate(singleCert)
|
|
386
|
+
if (client) {
|
|
387
|
+
const validation = await validateCertificateChainMatchesClientIdScheme(cert, client.clientId, client.clientIdScheme)
|
|
388
|
+
if (validation.error) {
|
|
389
|
+
return validation
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (blindlyTrustedAnchors.includes(singleCert)) {
|
|
393
|
+
console.log(`Certificate chain validation success as single cert if blindly trusted. WARNING: ONLY USE FOR TESTING PURPOSES.`)
|
|
394
|
+
return {
|
|
395
|
+
error: false,
|
|
396
|
+
critical: true,
|
|
397
|
+
message: `Certificate chain validation success as single cert if blindly trusted. WARNING: ONLY USE FOR TESTING PURPOSES.`,
|
|
398
|
+
verificationTime,
|
|
399
|
+
certificateChain: [await getCertificateInfo(cert)],
|
|
400
|
+
...(client && {client}),
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (allowSingleNoCAChainElement) {
|
|
404
|
+
const subjectDN = getSubjectDN(cert).DN
|
|
405
|
+
if (!getIssuerDN(cert).DN || getIssuerDN(cert).DN === subjectDN) {
|
|
406
|
+
const passed = await cert.verify()
|
|
407
|
+
return {
|
|
408
|
+
error: !passed,
|
|
409
|
+
critical: true,
|
|
410
|
+
message: `Certificate chain validation for ${subjectDN}: ${passed ? 'successful' : 'failed'}.`,
|
|
411
|
+
verificationTime,
|
|
412
|
+
certificateChain: [await getCertificateInfo(cert)],
|
|
413
|
+
...(client && {client}),
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const validationEngine = new CertificateChainValidationEngine({
|
|
420
|
+
certs /!*crls: [crl1], ocsps: [ocsp1], *!/,
|
|
421
|
+
checkDate: verificationTime,
|
|
422
|
+
trustedCerts,
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const verification = await validationEngine.verify()
|
|
427
|
+
if (!verification.result || !verification.certificatePath) {
|
|
428
|
+
return {
|
|
429
|
+
error: true,
|
|
430
|
+
critical: true,
|
|
431
|
+
message: verification.resultMessage !== '' ? verification.resultMessage : `Certificate chain validation failed.`,
|
|
432
|
+
verificationTime,
|
|
433
|
+
...(client && {client}),
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const certPath = verification.certificatePath
|
|
437
|
+
if (client) {
|
|
438
|
+
const clientIdValidation = await validateCertificateChainMatchesClientIdScheme(certs[0], client.clientId, client.clientIdScheme)
|
|
439
|
+
if (clientIdValidation.error) {
|
|
440
|
+
return clientIdValidation
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
let certInfos: Array<CertificateInfo> | undefined
|
|
444
|
+
|
|
445
|
+
for (const certificate of certPath) {
|
|
446
|
+
try {
|
|
447
|
+
certInfos?.push(await getCertificateInfo(certificate))
|
|
448
|
+
} catch (e: any) {
|
|
449
|
+
console.log(`Error getting certificate info ${e.message}`)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
error: false,
|
|
456
|
+
critical: false,
|
|
457
|
+
message: `Certificate chain was valid`,
|
|
458
|
+
verificationTime,
|
|
459
|
+
certificateChain: certInfos,
|
|
460
|
+
...(client && {client}),
|
|
461
|
+
}
|
|
462
|
+
} catch (error: any) {
|
|
463
|
+
return {
|
|
464
|
+
error: true,
|
|
465
|
+
critical: true,
|
|
466
|
+
message: `Certificate chain was invalid, ${error.message ?? '<unknown error>'}`,
|
|
467
|
+
verificationTime,
|
|
468
|
+
...(client && {client}),
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
*/
|
|
224
473
|
|
|
225
474
|
const rdnmap: Record<string, string> = {
|
|
226
475
|
'2.5.4.6': 'C',
|
|
@@ -264,7 +513,7 @@ const getDNString = (typesAndValues: AttributeTypeAndValue[]): string => {
|
|
|
264
513
|
.join(',')
|
|
265
514
|
}
|
|
266
515
|
|
|
267
|
-
export const getCertificateSubjectPublicKeyJWK = async (pemOrDerCert: string | Uint8Array | Certificate): Promise<
|
|
516
|
+
export const getCertificateSubjectPublicKeyJWK = async (pemOrDerCert: string | Uint8Array | Certificate): Promise<JWK> => {
|
|
268
517
|
const pemOrDerStr =
|
|
269
518
|
typeof pemOrDerCert === 'string'
|
|
270
519
|
? pemOrDerCert
|
|
@@ -273,14 +522,25 @@ export const getCertificateSubjectPublicKeyJWK = async (pemOrDerCert: string | U
|
|
|
273
522
|
: pemOrDerCert.toString('base64')
|
|
274
523
|
const pem = derToPEM(pemOrDerStr)
|
|
275
524
|
const certificate = pemOrDerToX509Certificate(pem)
|
|
525
|
+
var jwk: JWK | undefined
|
|
276
526
|
try {
|
|
277
527
|
const subtle = getCrypto(true).subtle
|
|
278
|
-
const pk = await certificate.getPublicKey()
|
|
279
|
-
|
|
528
|
+
const pk = await certificate.getPublicKey(undefined, defaultCryptoEngine())
|
|
529
|
+
jwk = (await subtle.exportKey('jwk', pk)) as JWK | undefined
|
|
280
530
|
} catch (error: any) {
|
|
281
531
|
console.log(`Error in primary get JWK from cert:`, error?.message)
|
|
282
532
|
}
|
|
283
|
-
|
|
533
|
+
if (!jwk) {
|
|
534
|
+
try {
|
|
535
|
+
jwk = (await x509.toJwk(pem, 'pem')) as JWK
|
|
536
|
+
} catch (error: any) {
|
|
537
|
+
console.log(`Error in secondary get JWK from cert as well:`, error?.message)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!jwk) {
|
|
541
|
+
throw Error(`Failed to get JWK from certificate ${pem}`)
|
|
542
|
+
}
|
|
543
|
+
return jwk
|
|
284
544
|
}
|
|
285
545
|
|
|
286
546
|
/**
|