@sphereon/ssi-sdk-ext.x509-utils 0.24.1-unstable.9
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/LICENSE +201 -0
- package/README.md +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/index.js.map +1 -0
- package/dist/x509/index.d.ts +5 -0
- package/dist/x509/index.d.ts.map +1 -0
- package/dist/x509/index.js +21 -0
- package/dist/x509/index.js.map +1 -0
- package/dist/x509/rsa-key.d.ts +10 -0
- package/dist/x509/rsa-key.d.ts.map +1 -0
- package/dist/x509/rsa-key.js +101 -0
- package/dist/x509/rsa-key.js.map +1 -0
- package/dist/x509/rsa-signer.d.ts +24 -0
- package/dist/x509/rsa-signer.d.ts.map +1 -0
- package/dist/x509/rsa-signer.js +105 -0
- package/dist/x509/rsa-signer.js.map +1 -0
- package/dist/x509/x509-utils.d.ts +29 -0
- package/dist/x509/x509-utils.d.ts.map +1 -0
- package/dist/x509/x509-utils.js +196 -0
- package/dist/x509/x509-utils.js.map +1 -0
- package/dist/x509/x509-validator.d.ts +42 -0
- package/dist/x509/x509-validator.d.ts.map +1 -0
- package/dist/x509/x509-validator.js +134 -0
- package/dist/x509/x509-validator.js.map +1 -0
- package/package.json +40 -0
- package/src/index.ts +6 -0
- package/src/types/index.ts +16 -0
- package/src/x509/index.ts +4 -0
- package/src/x509/rsa-key.ts +81 -0
- package/src/x509/rsa-signer.ts +80 -0
- package/src/x509/x509-utils.ts +165 -0
- package/src/x509/x509-validator.ts +162 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Certificate } from 'pkijs'
|
|
2
|
+
import * as u8a from 'uint8arrays'
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import keyto from '@trust/keyto'
|
|
5
|
+
import { KeyVisibility } from '../types'
|
|
6
|
+
|
|
7
|
+
// Based on (MIT licensed):
|
|
8
|
+
// https://github.com/hildjj/node-posh/blob/master/lib/index.js
|
|
9
|
+
export function pemCertChainTox5c(cert: string, maxDepth?: number): string[] {
|
|
10
|
+
if (!maxDepth) {
|
|
11
|
+
maxDepth = 0
|
|
12
|
+
}
|
|
13
|
+
/*
|
|
14
|
+
* Convert a PEM-encoded certificate to the version used in the x5c element
|
|
15
|
+
* of a [JSON Web Key](http://tools.ietf.org/html/draft-ietf-jose-json-web-key).
|
|
16
|
+
*
|
|
17
|
+
* `cert` PEM-encoded certificate chain
|
|
18
|
+
* `maxdepth` The maximum number of certificates to use from the chain.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const intermediate = cert
|
|
22
|
+
.replace(/-----[^\n]+\n?/gm, ',')
|
|
23
|
+
.replace(/\n/g, '')
|
|
24
|
+
.replace(/\r/g, '')
|
|
25
|
+
let x5c = intermediate.split(',').filter(function (c) {
|
|
26
|
+
return c.length > 0
|
|
27
|
+
})
|
|
28
|
+
if (maxDepth > 0) {
|
|
29
|
+
x5c = x5c.splice(0, maxDepth)
|
|
30
|
+
}
|
|
31
|
+
return x5c
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function x5cToPemCertChain(x5c: string[], maxDepth?: number): string {
|
|
35
|
+
if (!maxDepth) {
|
|
36
|
+
maxDepth = 0
|
|
37
|
+
}
|
|
38
|
+
const length = maxDepth === 0 ? x5c.length : Math.min(maxDepth, x5c.length)
|
|
39
|
+
let pem = ''
|
|
40
|
+
for (let i = 0; i < length; i++) {
|
|
41
|
+
pem += derToPEM(x5c[i], 'CERTIFICATE')
|
|
42
|
+
}
|
|
43
|
+
return pem
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const pemOrDerToX509Certificate = (cert: string | Uint8Array): Certificate => {
|
|
47
|
+
if (typeof cert !== 'string') {
|
|
48
|
+
return Certificate.fromBER(cert)
|
|
49
|
+
}
|
|
50
|
+
let DER = cert
|
|
51
|
+
if (cert.includes('CERTIFICATE')) {
|
|
52
|
+
DER = PEMToDer(cert)
|
|
53
|
+
}
|
|
54
|
+
return Certificate.fromBER(u8a.fromString(DER, 'base64pad'))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const areCertificatesEqual = (cert1: Certificate, cert2: Certificate): boolean => {
|
|
58
|
+
return cert1.signatureValue.isEqual(cert2.signatureValue)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const toKeyObject = (PEM: string, visibility: KeyVisibility = 'public') => {
|
|
62
|
+
const jwk = PEMToJwk(PEM, visibility)
|
|
63
|
+
const keyVisibility: KeyVisibility = jwk.d ? 'private' : 'public'
|
|
64
|
+
const keyHex = keyVisibility === 'private' ? privateKeyHexFromPEM(PEM) : publicKeyHexFromPEM(PEM)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
pem: hexToPEM(keyHex, visibility),
|
|
68
|
+
jwk,
|
|
69
|
+
keyHex,
|
|
70
|
+
keyType: keyVisibility,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const jwkToPEM = (jwk: JsonWebKey, visibility: KeyVisibility = 'public'): string => {
|
|
75
|
+
return keyto.from(jwk, 'jwk').toString('pem', visibility === 'public' ? 'public_pkcs8' : 'private_pkcs8')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const PEMToJwk = (pem: string, visibility: KeyVisibility = 'public'): JsonWebKey => {
|
|
79
|
+
return keyto.from(pem, 'pem').toJwk(visibility)
|
|
80
|
+
}
|
|
81
|
+
export const privateKeyHexFromPEM = (PEM: string) => {
|
|
82
|
+
return PEMToHex(PEM)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const hexKeyFromPEMBasedJwk = (jwk: JsonWebKey, visibility: KeyVisibility = 'public'): string => {
|
|
86
|
+
if (visibility === 'private') {
|
|
87
|
+
return privateKeyHexFromPEM(jwkToPEM(jwk, 'private'))
|
|
88
|
+
} else {
|
|
89
|
+
return publicKeyHexFromPEM(jwkToPEM(jwk, 'public'))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const publicKeyHexFromPEM = (PEM: string) => {
|
|
94
|
+
const hex = PEMToHex(PEM)
|
|
95
|
+
if (PEM.includes('CERTIFICATE')) {
|
|
96
|
+
throw Error('Cannot directly deduce public Key from PEM Certificate yet')
|
|
97
|
+
} else if (!PEM.includes('PRIVATE')) {
|
|
98
|
+
return hex
|
|
99
|
+
}
|
|
100
|
+
const publicJwk = PEMToJwk(PEM, 'public')
|
|
101
|
+
const publicPEM = jwkToPEM(publicJwk, 'public')
|
|
102
|
+
return PEMToHex(publicPEM)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const PEMToHex = (PEM: string, headerKey?: string): string => {
|
|
106
|
+
if (PEM.indexOf('-----BEGIN ') == -1) {
|
|
107
|
+
throw Error(`PEM header not found: ${headerKey}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let strippedPem: string
|
|
111
|
+
if (headerKey) {
|
|
112
|
+
strippedPem = PEM.replace(new RegExp('^[^]*-----BEGIN ' + headerKey + '-----'), '')
|
|
113
|
+
strippedPem = strippedPem.replace(new RegExp('-----END ' + headerKey + '-----[^]*$'), '')
|
|
114
|
+
} else {
|
|
115
|
+
strippedPem = PEM.replace(/^[^]*-----BEGIN [^-]+-----/, '')
|
|
116
|
+
strippedPem = strippedPem.replace(/-----END [^-]+-----[^]*$/, '')
|
|
117
|
+
}
|
|
118
|
+
return base64ToHex(strippedPem, 'base64pad')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Converts a base64 encoded string to hex string, removing any non-base64 characters, including newlines
|
|
123
|
+
* @param input The input in base64, with optional newlines
|
|
124
|
+
* @param inputEncoding
|
|
125
|
+
*/
|
|
126
|
+
export const base64ToHex = (input: string, inputEncoding?: 'base64' | 'base64pad' | 'base64url' | 'base64urlpad') => {
|
|
127
|
+
const base64NoNewlines = input.replace(/[^0-9A-Za-z_\-~\/+=]*/g, '')
|
|
128
|
+
return u8a.toString(u8a.fromString(base64NoNewlines, inputEncoding ? inputEncoding : 'base64pad'), 'base16')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const hexToBase64 = (input: number | object | string, targetEncoding?: 'base64' | 'base64pad' | 'base64url' | 'base64urlpad'): string => {
|
|
132
|
+
let hex = typeof input === 'string' ? input : input.toString(16)
|
|
133
|
+
if (hex.length % 2 === 1) {
|
|
134
|
+
hex = `0${hex}`
|
|
135
|
+
}
|
|
136
|
+
return u8a.toString(u8a.fromString(hex, 'base16'), targetEncoding ? targetEncoding : 'base64pad')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const hexToPEM = (hex: string, type: KeyVisibility): string => {
|
|
140
|
+
const base64 = hexToBase64(hex, 'base64pad')
|
|
141
|
+
const headerKey = type === 'private' ? 'RSA PRIVATE KEY' : 'PUBLIC KEY'
|
|
142
|
+
if (type === 'private') {
|
|
143
|
+
const pem = derToPEM(base64, headerKey)
|
|
144
|
+
try {
|
|
145
|
+
PEMToJwk(pem) // We only use it to test the private key
|
|
146
|
+
return pem
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return derToPEM(base64, 'PRIVATE KEY')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return derToPEM(base64, headerKey)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function PEMToDer(pem: string): string {
|
|
155
|
+
return pem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\n\r])/g, '')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function derToPEM(cert: string, headerKey?: 'PUBLIC KEY' | 'RSA PRIVATE KEY' | 'PRIVATE KEY' | 'CERTIFICATE'): string {
|
|
159
|
+
const key = headerKey ?? 'CERTIFICATE'
|
|
160
|
+
const matches = cert.match(/.{1,64}/g)
|
|
161
|
+
if (!matches) {
|
|
162
|
+
throw Error('Invalid cert input value supplied')
|
|
163
|
+
}
|
|
164
|
+
return `-----BEGIN ${key}-----\n${matches.join('\n')}\n-----END ${key}-----\n`
|
|
165
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { AttributeTypeAndValue, Certificate, CertificateChainValidationEngine, CryptoEngine, getCrypto, setEngine } from 'pkijs'
|
|
2
|
+
import { pemOrDerToX509Certificate } from './x509-utils'
|
|
3
|
+
|
|
4
|
+
export type DNInfo = {
|
|
5
|
+
DN: string
|
|
6
|
+
attributes: Record<string, string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type CertInfo = {
|
|
10
|
+
certificate?: Certificate
|
|
11
|
+
notBefore: Date
|
|
12
|
+
notAfter: Date
|
|
13
|
+
publicKeyJWK?: any
|
|
14
|
+
issuer: {
|
|
15
|
+
dn: DNInfo
|
|
16
|
+
}
|
|
17
|
+
subject: {
|
|
18
|
+
dn: DNInfo
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type X509ValidationResult = {
|
|
23
|
+
error: boolean
|
|
24
|
+
critical: boolean
|
|
25
|
+
message: string
|
|
26
|
+
verificationTime: Date
|
|
27
|
+
certificateChain?: Array<CertInfo>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultCryptoEngine = () => {
|
|
31
|
+
if (typeof self !== 'undefined') {
|
|
32
|
+
if ('crypto' in self) {
|
|
33
|
+
let engineName = 'webcrypto'
|
|
34
|
+
if ('webkitSubtle' in self.crypto) {
|
|
35
|
+
engineName = 'safari'
|
|
36
|
+
}
|
|
37
|
+
setEngine(engineName, new CryptoEngine({ name: engineName, crypto: crypto }))
|
|
38
|
+
}
|
|
39
|
+
} else if (typeof crypto !== 'undefined' && 'webcrypto' in crypto) {
|
|
40
|
+
const name = 'NodeJS ^15'
|
|
41
|
+
const nodeCrypto = crypto.webcrypto
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
setEngine(name, new CryptoEngine({ name, crypto: nodeCrypto }))
|
|
44
|
+
} else if (typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined') {
|
|
45
|
+
const name = 'crypto'
|
|
46
|
+
setEngine(name, new CryptoEngine({ name, crypto: crypto }))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @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
|
|
52
|
+
* @param trustedPEMs
|
|
53
|
+
* @param verificationTime
|
|
54
|
+
* @param opts
|
|
55
|
+
*/
|
|
56
|
+
export const validateX509CertificateChain = async ({
|
|
57
|
+
chain: pemOrDerChain,
|
|
58
|
+
trustAnchors,
|
|
59
|
+
verificationTime = new Date(),
|
|
60
|
+
opts = { trustRootWhenNoAnchors: false },
|
|
61
|
+
}: {
|
|
62
|
+
chain: (Uint8Array | string)[]
|
|
63
|
+
trustAnchors?: string[]
|
|
64
|
+
verificationTime?: Date
|
|
65
|
+
opts?: { trustRootWhenNoAnchors: boolean }
|
|
66
|
+
}): Promise<X509ValidationResult> => {
|
|
67
|
+
const { trustRootWhenNoAnchors = false } = opts
|
|
68
|
+
const trustedPEMs = trustRootWhenNoAnchors && !trustAnchors ? [pemOrDerChain[pemOrDerChain.length - 1]] : trustAnchors
|
|
69
|
+
|
|
70
|
+
if (pemOrDerChain.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
error: true,
|
|
73
|
+
critical: true,
|
|
74
|
+
message: 'Certificate chain in DER or PEM format must not be empty',
|
|
75
|
+
verificationTime,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const certs = pemOrDerChain.map(pemOrDerToX509Certificate)
|
|
80
|
+
const trustedCerts = trustedPEMs ? trustedPEMs.map(pemOrDerToX509Certificate) : undefined
|
|
81
|
+
defaultCryptoEngine()
|
|
82
|
+
|
|
83
|
+
const validationEngine = new CertificateChainValidationEngine({
|
|
84
|
+
certs /*crls: [crl1], ocsps: [ocsp1], */,
|
|
85
|
+
checkDate: verificationTime,
|
|
86
|
+
trustedCerts,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const verification = await validationEngine.verify()
|
|
90
|
+
if (!verification.result || !verification.certificatePath) {
|
|
91
|
+
return {
|
|
92
|
+
error: true,
|
|
93
|
+
critical: true,
|
|
94
|
+
message: verification.resultMessage !== '' ? verification.resultMessage : `Certificate chain validation failed.`,
|
|
95
|
+
verificationTime,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const subtle = getCrypto(true).subtle
|
|
99
|
+
const certPath = verification.certificatePath
|
|
100
|
+
const certInfos: Array<CertInfo> = await Promise.all(
|
|
101
|
+
certPath.map(async (certificate) => {
|
|
102
|
+
const pk = await certificate.getPublicKey()
|
|
103
|
+
return {
|
|
104
|
+
issuer: { dn: getIssuerDN(certificate) },
|
|
105
|
+
subject: { dn: getSubjectDN(certificate) },
|
|
106
|
+
publicKeyJWK: await subtle.exportKey('jwk', pk),
|
|
107
|
+
notBefore: certificate.notBefore.value,
|
|
108
|
+
notAfter: certificate.notAfter.value,
|
|
109
|
+
// certificate
|
|
110
|
+
} satisfies CertInfo
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
error: false,
|
|
115
|
+
critical: false,
|
|
116
|
+
message: `Certificate chain was valid`,
|
|
117
|
+
verificationTime,
|
|
118
|
+
certificateChain: certInfos,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rdnmap: Record<string, string> = {
|
|
123
|
+
'2.5.4.6': 'C',
|
|
124
|
+
'2.5.4.10': 'O',
|
|
125
|
+
'2.5.4.11': 'OU',
|
|
126
|
+
'2.5.4.3': 'CN',
|
|
127
|
+
'2.5.4.7': 'L',
|
|
128
|
+
'2.5.4.8': 'ST',
|
|
129
|
+
'2.5.4.12': 'T',
|
|
130
|
+
'2.5.4.42': 'GN',
|
|
131
|
+
'2.5.4.43': 'I',
|
|
132
|
+
'2.5.4.4': 'SN',
|
|
133
|
+
'1.2.840.113549.1.9.1': 'E-mail',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const getIssuerDN = (cert: Certificate): DNInfo => {
|
|
137
|
+
return {
|
|
138
|
+
DN: getDNString(cert.issuer.typesAndValues),
|
|
139
|
+
attributes: getDNObject(cert.issuer.typesAndValues),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const getSubjectDN = (cert: Certificate): DNInfo => {
|
|
144
|
+
return {
|
|
145
|
+
DN: getDNString(cert.subject.typesAndValues),
|
|
146
|
+
attributes: getDNObject(cert.subject.typesAndValues),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const getDNObject = (typesAndValues: AttributeTypeAndValue[]): Record<string, string> => {
|
|
151
|
+
const DN: Record<string, string> = {}
|
|
152
|
+
for (const typeAndValue of typesAndValues) {
|
|
153
|
+
const type = rdnmap[typeAndValue.type] ?? typeAndValue.type
|
|
154
|
+
DN[type] = typeAndValue.value.getValue()
|
|
155
|
+
}
|
|
156
|
+
return DN
|
|
157
|
+
}
|
|
158
|
+
const getDNString = (typesAndValues: AttributeTypeAndValue[]): string => {
|
|
159
|
+
return Object.entries(getDNObject(typesAndValues))
|
|
160
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
161
|
+
.join(',')
|
|
162
|
+
}
|