@sphereon/ssi-sdk-ext.x509-utils 0.26.1-next.5 → 0.26.1-next.86
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 +265 -66
- 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 +356 -97
|
@@ -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,95 +153,323 @@ export const validateX509CertificateChain = async ({
|
|
|
133
153
|
verificationTime,
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
|
-
|
|
137
|
-
// x5c always starts with the leaf cert at index 0 and then the cas. Our internal pkijs service expects it the other way around
|
|
138
|
-
const certs = pemOrDerChain.map(pemOrDerToX509Certificate).reverse()
|
|
139
|
-
const trustedCerts = trustedPEMs ? trustedPEMs.map(pemOrDerToX509Certificate) : undefined
|
|
140
156
|
defaultCryptoEngine()
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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) {
|
|
152
186
|
console.log(`Certificate chain validation success as single cert if blindly trusted. WARNING: ONLY USE FOR TESTING PURPOSES.`)
|
|
153
187
|
return {
|
|
154
188
|
error: false,
|
|
155
|
-
critical:
|
|
189
|
+
critical: false,
|
|
156
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,
|
|
157
193
|
verificationTime,
|
|
158
|
-
certificateChain:
|
|
194
|
+
certificateChain: x5cOrdereredChain.map((cert) => cert.certificateInfo),
|
|
159
195
|
...(client && { client }),
|
|
160
196
|
}
|
|
161
197
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
209
|
return {
|
|
167
|
-
error:
|
|
210
|
+
error: true,
|
|
168
211
|
critical: true,
|
|
169
|
-
|
|
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}.`,
|
|
170
215
|
verificationTime,
|
|
171
|
-
certificateChain: [await getCertificateInfo(cert)],
|
|
172
216
|
...(client && { client }),
|
|
173
217
|
}
|
|
174
218
|
}
|
|
175
219
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
238
|
|
|
184
|
-
try {
|
|
185
|
-
const verification = await validationEngine.verify()
|
|
186
|
-
if (!verification.result || !verification.certificatePath) {
|
|
187
239
|
return {
|
|
188
240
|
error: true,
|
|
189
241
|
critical: true,
|
|
190
|
-
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)}.`,
|
|
191
247
|
verificationTime,
|
|
192
248
|
...(client && { client }),
|
|
193
249
|
}
|
|
194
250
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 }),
|
|
200
263
|
}
|
|
201
264
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
})
|
|
206
|
-
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (foundTrustAnchor?.certificateInfo || allowNoTrustAnchorsFound) {
|
|
207
268
|
return {
|
|
208
269
|
error: false,
|
|
209
270
|
critical: false,
|
|
210
271
|
message: `Certificate chain was valid`,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
error: true,
|
|
218
|
-
critical: true,
|
|
219
|
-
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,
|
|
220
277
|
verificationTime,
|
|
221
278
|
...(client && { client }),
|
|
222
279
|
}
|
|
223
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
|
+
}
|
|
224
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
|
+
*/
|
|
225
473
|
|
|
226
474
|
const rdnmap: Record<string, string> = {
|
|
227
475
|
'2.5.4.6': 'C',
|
|
@@ -265,23 +513,34 @@ const getDNString = (typesAndValues: AttributeTypeAndValue[]): string => {
|
|
|
265
513
|
.join(',')
|
|
266
514
|
}
|
|
267
515
|
|
|
268
|
-
export const getCertificateSubjectPublicKeyJWK = async (pemOrDerCert: string | Uint8Array | Certificate): Promise<
|
|
516
|
+
export const getCertificateSubjectPublicKeyJWK = async (pemOrDerCert: string | Uint8Array | Certificate): Promise<JWK> => {
|
|
269
517
|
const pemOrDerStr =
|
|
270
518
|
typeof pemOrDerCert === 'string'
|
|
271
|
-
? pemOrDerCert
|
|
519
|
+
? u8a.toString(u8a.fromString(pemOrDerCert, 'base64pad'), 'base64pad')
|
|
272
520
|
: pemOrDerCert instanceof Uint8Array
|
|
273
521
|
? u8a.toString(pemOrDerCert, 'base64pad')
|
|
274
|
-
: pemOrDerCert.toString('base64')
|
|
522
|
+
: u8a.toString(u8a.fromString(pemOrDerCert.toString('base64'), 'base64pad'), 'base64pad')
|
|
275
523
|
const pem = derToPEM(pemOrDerStr)
|
|
276
524
|
const certificate = pemOrDerToX509Certificate(pem)
|
|
525
|
+
var jwk: JWK | undefined
|
|
277
526
|
try {
|
|
278
527
|
const subtle = getCrypto(true).subtle
|
|
279
|
-
const pk = await certificate.getPublicKey()
|
|
280
|
-
|
|
528
|
+
const pk = await certificate.getPublicKey(undefined, defaultCryptoEngine())
|
|
529
|
+
jwk = (await subtle.exportKey('jwk', pk)) as JWK | undefined
|
|
281
530
|
} catch (error: any) {
|
|
282
531
|
console.log(`Error in primary get JWK from cert:`, error?.message)
|
|
283
532
|
}
|
|
284
|
-
|
|
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
|
|
285
544
|
}
|
|
286
545
|
|
|
287
546
|
/**
|