@sphereon/ssi-sdk-ext.key-utils 0.10.2-unstable.14
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/digest-methods.d.ts +7 -0
- package/dist/digest-methods.d.ts.map +1 -0
- package/dist/digest-methods.js +95 -0
- package/dist/digest-methods.js.map +1 -0
- package/dist/functions.d.ts +30 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +150 -0
- package/dist/functions.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/jwk-jcs.d.ts +22 -0
- package/dist/jwk-jcs.d.ts.map +1 -0
- package/dist/jwk-jcs.js +178 -0
- package/dist/jwk-jcs.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/key-util-types.d.ts +36 -0
- package/dist/types/key-util-types.d.ts.map +1 -0
- package/dist/types/key-util-types.js +30 -0
- package/dist/types/key-util-types.js.map +1 -0
- package/dist/x509-utils.d.ts +24 -0
- package/dist/x509-utils.d.ts.map +1 -0
- package/dist/x509-utils.js +175 -0
- package/dist/x509-utils.js.map +1 -0
- package/package.json +49 -0
- package/src/digest-methods.ts +74 -0
- package/src/functions.ts +147 -0
- package/src/index.ts +11 -0
- package/src/jwk-jcs.ts +177 -0
- package/src/types/elliptic.d.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/key-util-types.ts +44 -0
- package/src/x509-utils.ts +145 -0
package/src/functions.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomBytes } from '@ethersproject/random'
|
|
2
|
+
import { generateKeyPair as generateSigningKeyPair } from '@stablelib/ed25519'
|
|
3
|
+
|
|
4
|
+
import { JsonWebKey } from 'did-resolver'
|
|
5
|
+
import * as u8a from 'uint8arrays'
|
|
6
|
+
import { ENC_KEY_ALGS, Key, KeyCurve, KeyType, JwkKeyUse, SIG_KEY_ALGS, TKeyType } from './types'
|
|
7
|
+
import elliptic from 'elliptic'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a random Private Hex Key for the specified key type
|
|
11
|
+
* @param type The key type
|
|
12
|
+
* @return The private key in Hex form
|
|
13
|
+
*/
|
|
14
|
+
export const generatePrivateKeyHex = (type: TKeyType): string => {
|
|
15
|
+
switch (type) {
|
|
16
|
+
case Key.Ed25519: {
|
|
17
|
+
const keyPairEd25519 = generateSigningKeyPair()
|
|
18
|
+
return u8a.toString(keyPairEd25519.secretKey, 'base16')
|
|
19
|
+
}
|
|
20
|
+
// The Secp256 types use the same method to generate the key
|
|
21
|
+
case Key.Secp256r1:
|
|
22
|
+
case Key.Secp256k1: {
|
|
23
|
+
const privateBytes = randomBytes(32)
|
|
24
|
+
return u8a.toString(privateBytes, 'base16')
|
|
25
|
+
}
|
|
26
|
+
default:
|
|
27
|
+
throw Error(`not_supported: Key type ${type} not yet supported for this did:jwk implementation`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts hex value to base64url
|
|
33
|
+
* @param value hex value
|
|
34
|
+
* @return Base64Url encoded value
|
|
35
|
+
*/
|
|
36
|
+
export const hex2base64url = (value: string) => {
|
|
37
|
+
const buffer = Buffer.from(value, 'hex')
|
|
38
|
+
const base64 = buffer.toString('base64')
|
|
39
|
+
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
|
40
|
+
|
|
41
|
+
return base64url
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts a public key in hex format to a JWK
|
|
46
|
+
* @param publicKeyHex public key in hex
|
|
47
|
+
* @param type The type of the key (Ed25519, Secp256k1/r1)
|
|
48
|
+
* @param use The optional use for the key (sig/enc)
|
|
49
|
+
* @return The JWK
|
|
50
|
+
*/
|
|
51
|
+
export const toJwk = (publicKeyHex: string, type: TKeyType, use?: JwkKeyUse): JsonWebKey => {
|
|
52
|
+
switch (type) {
|
|
53
|
+
case Key.Ed25519:
|
|
54
|
+
return toEd25519Jwk(publicKeyHex, use)
|
|
55
|
+
case Key.Secp256k1:
|
|
56
|
+
return toSecp256k1Jwk(publicKeyHex, use)
|
|
57
|
+
case Key.Secp256r1:
|
|
58
|
+
return toSecp256r1Jwk(publicKeyHex, use)
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`not_supported: Key type ${type} not yet supported for this did:jwk implementation`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Determines the use param based upon the key/signature type or supplied use value.
|
|
66
|
+
*
|
|
67
|
+
* @param type The key type
|
|
68
|
+
* @param suppliedUse A supplied use. Will be used in case it is present
|
|
69
|
+
*/
|
|
70
|
+
export const jwkDetermineUse = (type: TKeyType, suppliedUse?: JwkKeyUse): JwkKeyUse | undefined => {
|
|
71
|
+
return suppliedUse
|
|
72
|
+
? suppliedUse
|
|
73
|
+
: SIG_KEY_ALGS.includes(type)
|
|
74
|
+
? JwkKeyUse.Signature
|
|
75
|
+
: ENC_KEY_ALGS.includes(type)
|
|
76
|
+
? JwkKeyUse.Encryption
|
|
77
|
+
: undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Assert the key has a proper length
|
|
82
|
+
*
|
|
83
|
+
* @param keyHex Input key
|
|
84
|
+
* @param expectedKeyLength Expected key length
|
|
85
|
+
*/
|
|
86
|
+
const assertProperKeyLength = (keyHex: string, expectedKeyLength: number) => {
|
|
87
|
+
if (keyHex.length !== expectedKeyLength) {
|
|
88
|
+
throw Error(`Invalid key length. Needs to be a hex string with length ${expectedKeyLength} instead of ${keyHex.length}. Input: ${keyHex}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generates a JWK from a Secp256k1 public key
|
|
94
|
+
* @param publicKeyHex Secp256k1 public key in hex
|
|
95
|
+
* @param use The use for the key
|
|
96
|
+
* @return The JWK
|
|
97
|
+
*/
|
|
98
|
+
const toSecp256k1Jwk = (publicKeyHex: string, use?: JwkKeyUse): JsonWebKey => {
|
|
99
|
+
assertProperKeyLength(publicKeyHex, 130)
|
|
100
|
+
return {
|
|
101
|
+
alg: 'ES256K',
|
|
102
|
+
...(use !== undefined && { use }),
|
|
103
|
+
kty: KeyType.EC,
|
|
104
|
+
crv: KeyCurve.Secp256k1,
|
|
105
|
+
x: hex2base64url(publicKeyHex.substr(2, 64)),
|
|
106
|
+
y: hex2base64url(publicKeyHex.substr(66, 64)),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generates a JWK from a Secp256r1 public key
|
|
112
|
+
* @param publicKeyHex Secp256r1 public key in hex
|
|
113
|
+
* @param use The use for the key
|
|
114
|
+
* @return The JWK
|
|
115
|
+
*/
|
|
116
|
+
const toSecp256r1Jwk = (publicKeyHex: string, use?: JwkKeyUse): JsonWebKey => {
|
|
117
|
+
assertProperKeyLength(publicKeyHex, 64)
|
|
118
|
+
const secp256r1 = new elliptic.ec('p256')
|
|
119
|
+
const publicKey = `03${publicKeyHex}` // We add the 'compressed' type 03 prefix
|
|
120
|
+
const key = secp256r1.keyFromPublic(publicKey, 'hex')
|
|
121
|
+
const pubPoint = key.getPublic()
|
|
122
|
+
return {
|
|
123
|
+
alg: 'ES256',
|
|
124
|
+
...(use !== undefined && { use }),
|
|
125
|
+
kty: KeyType.EC,
|
|
126
|
+
crv: KeyCurve.P_256,
|
|
127
|
+
x: hex2base64url(pubPoint.getX().toString('hex')),
|
|
128
|
+
y: hex2base64url(pubPoint.getY().toString('hex')),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generates a JWK from an Ed25519 public key
|
|
134
|
+
* @param publicKeyHex Ed25519 public key in hex
|
|
135
|
+
* @param use The use for the key
|
|
136
|
+
* @return The JWK
|
|
137
|
+
*/
|
|
138
|
+
const toEd25519Jwk = (publicKeyHex: string, use?: JwkKeyUse): JsonWebKey => {
|
|
139
|
+
assertProperKeyLength(publicKeyHex, 64)
|
|
140
|
+
return {
|
|
141
|
+
alg: 'EdDSA',
|
|
142
|
+
...(use !== undefined && { use }),
|
|
143
|
+
kty: KeyType.OKP,
|
|
144
|
+
crv: KeyCurve.Ed25519,
|
|
145
|
+
x: hex2base64url(publicKeyHex.substr(0, 64)),
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides `did:jwk` {@link @veramo/did-provider-jwk#JwkDIDProvider | identifier provider }
|
|
3
|
+
* for the {@link @veramo/did-manager#DIDManager}
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
export * from './functions'
|
|
8
|
+
export * from './jwk-jcs'
|
|
9
|
+
export * from './types'
|
|
10
|
+
export * from './x509-utils'
|
|
11
|
+
export * from './digest-methods'
|
package/src/jwk-jcs.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { TextDecoder, TextEncoder } from 'web-encoding'
|
|
2
|
+
import isPlainObject from 'lodash.isplainobject'
|
|
3
|
+
import type { ByteView } from 'multiformats/codecs/interface'
|
|
4
|
+
import type { JsonWebKey } from 'did-resolver'
|
|
5
|
+
|
|
6
|
+
const textEncoder = new TextEncoder()
|
|
7
|
+
const textDecoder = new TextDecoder()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if the value is a non-empty string.
|
|
11
|
+
*
|
|
12
|
+
* @param value - The value to check.
|
|
13
|
+
* @param description - Description of the value to check.
|
|
14
|
+
*/
|
|
15
|
+
function check(value: unknown, description: string) {
|
|
16
|
+
if (typeof value !== 'string' || !value) {
|
|
17
|
+
throw new Error(`${description} missing or invalid`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the value is a valid JSON object.
|
|
23
|
+
*
|
|
24
|
+
* @param value - The value to check.
|
|
25
|
+
*/
|
|
26
|
+
function validatePlainObject(value: unknown) {
|
|
27
|
+
if (!isPlainObject(value)) {
|
|
28
|
+
throw new Error('JWK must be an object')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Checks if the JWK is valid. It must contain all the required members.
|
|
34
|
+
*
|
|
35
|
+
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6
|
|
36
|
+
* @see https://www.rfc-editor.org/rfc/rfc8037#section-2
|
|
37
|
+
*
|
|
38
|
+
* @param jwk - The JWK to check.
|
|
39
|
+
*/
|
|
40
|
+
function validateJwk(jwk: any) {
|
|
41
|
+
validatePlainObject(jwk)
|
|
42
|
+
// Check JWK required members based on the key type
|
|
43
|
+
switch (jwk.kty) {
|
|
44
|
+
/**
|
|
45
|
+
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1
|
|
46
|
+
*/
|
|
47
|
+
case 'EC':
|
|
48
|
+
check(jwk.crv, '"crv" (Curve) Parameter')
|
|
49
|
+
check(jwk.x, '"x" (X Coordinate) Parameter')
|
|
50
|
+
check(jwk.y, '"y" (Y Coordinate) Parameter')
|
|
51
|
+
break
|
|
52
|
+
/**
|
|
53
|
+
* @see https://www.rfc-editor.org/rfc/rfc8037#section-2
|
|
54
|
+
*/
|
|
55
|
+
case 'OKP':
|
|
56
|
+
check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter')
|
|
57
|
+
check(jwk.x, '"x" (Public Key) Parameter')
|
|
58
|
+
break
|
|
59
|
+
/**
|
|
60
|
+
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1
|
|
61
|
+
*/
|
|
62
|
+
case 'RSA':
|
|
63
|
+
check(jwk.e, '"e" (Exponent) Parameter')
|
|
64
|
+
check(jwk.n, '"n" (Modulus) Parameter')
|
|
65
|
+
break
|
|
66
|
+
default:
|
|
67
|
+
throw new Error('"kty" (Key Type) Parameter missing or unsupported')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extracts the required members of the JWK and canonicalizes it.
|
|
73
|
+
*
|
|
74
|
+
* @param jwk - The JWK to canonicalize.
|
|
75
|
+
* @returns The JWK with only the required members, ordered lexicographically.
|
|
76
|
+
*/
|
|
77
|
+
function minimalJwk(jwk: any) {
|
|
78
|
+
// "default" case is not needed
|
|
79
|
+
// eslint-disable-next-line default-case
|
|
80
|
+
switch (jwk.kty) {
|
|
81
|
+
case 'EC':
|
|
82
|
+
return { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }
|
|
83
|
+
case 'OKP':
|
|
84
|
+
return { crv: jwk.crv, kty: jwk.kty, x: jwk.x }
|
|
85
|
+
case 'RSA':
|
|
86
|
+
return { e: jwk.e, kty: jwk.kty, n: jwk.n }
|
|
87
|
+
}
|
|
88
|
+
throw Error(`Unsupported key type (kty) provided: ${jwk.kty}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Encodes a JWK into a Uint8Array. Only the required JWK members are encoded.
|
|
93
|
+
*
|
|
94
|
+
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6
|
|
95
|
+
* @see https://www.rfc-editor.org/rfc/rfc8037#section-2
|
|
96
|
+
* @see https://github.com/panva/jose/blob/3b8aa47b92d07a711bf5c3125276cc9a011794a4/src/jwk/thumbprint.ts#L37
|
|
97
|
+
*
|
|
98
|
+
* @param jwk - JSON Web Key.
|
|
99
|
+
* @returns Uint8Array-encoded JWK.
|
|
100
|
+
*/
|
|
101
|
+
export function jwkJcsEncode(jwk: unknown): Uint8Array {
|
|
102
|
+
validateJwk(jwk)
|
|
103
|
+
const strippedJwk = minimalJwk(jwk)
|
|
104
|
+
return textEncoder.encode(jcsCanonicalize(strippedJwk))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Decodes an array of bytes into a JWK. Throws an error if the JWK is not valid.
|
|
109
|
+
*
|
|
110
|
+
* @param bytes - The array of bytes to decode.
|
|
111
|
+
* @returns The corresponding JSON Web Key.
|
|
112
|
+
*/
|
|
113
|
+
export function jwkJcsDecode(bytes: ByteView<JsonWebKey>): JsonWebKey {
|
|
114
|
+
const jwk = JSON.parse(textDecoder.decode(bytes))
|
|
115
|
+
validateJwk(jwk)
|
|
116
|
+
if (JSON.stringify(jwk) !== jcsCanonicalize(minimalJwk(jwk))) {
|
|
117
|
+
throw new Error('The JWK embedded in the DID is not correctly formatted')
|
|
118
|
+
}
|
|
119
|
+
return jwk
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// From: https://github.com/cyberphone/json-canonicalization
|
|
123
|
+
export function jcsCanonicalize(object: any) {
|
|
124
|
+
let buffer = ''
|
|
125
|
+
serialize(object)
|
|
126
|
+
return buffer
|
|
127
|
+
|
|
128
|
+
function serialize(object: any) {
|
|
129
|
+
if (object === null || typeof object !== 'object' || object.toJSON != null) {
|
|
130
|
+
/////////////////////////////////////////////////
|
|
131
|
+
// Primitive type or toJSON - Use ES6/JSON //
|
|
132
|
+
/////////////////////////////////////////////////
|
|
133
|
+
buffer += JSON.stringify(object)
|
|
134
|
+
} else if (Array.isArray(object)) {
|
|
135
|
+
/////////////////////////////////////////////////
|
|
136
|
+
// Array - Maintain element order //
|
|
137
|
+
/////////////////////////////////////////////////
|
|
138
|
+
buffer += '['
|
|
139
|
+
let next = false
|
|
140
|
+
object.forEach((element) => {
|
|
141
|
+
if (next) {
|
|
142
|
+
buffer += ','
|
|
143
|
+
}
|
|
144
|
+
next = true
|
|
145
|
+
/////////////////////////////////////////
|
|
146
|
+
// Array element - Recursive expansion //
|
|
147
|
+
/////////////////////////////////////////
|
|
148
|
+
serialize(element)
|
|
149
|
+
})
|
|
150
|
+
buffer += ']'
|
|
151
|
+
} else {
|
|
152
|
+
/////////////////////////////////////////////////
|
|
153
|
+
// Object - Sort properties before serializing //
|
|
154
|
+
/////////////////////////////////////////////////
|
|
155
|
+
buffer += '{'
|
|
156
|
+
let next = false
|
|
157
|
+
Object.keys(object)
|
|
158
|
+
.sort()
|
|
159
|
+
.forEach((property) => {
|
|
160
|
+
if (next) {
|
|
161
|
+
buffer += ','
|
|
162
|
+
}
|
|
163
|
+
next = true
|
|
164
|
+
///////////////////////////////////////////////
|
|
165
|
+
// Property names are strings - Use ES6/JSON //
|
|
166
|
+
///////////////////////////////////////////////
|
|
167
|
+
buffer += JSON.stringify(property)
|
|
168
|
+
buffer += ':'
|
|
169
|
+
//////////////////////////////////////////
|
|
170
|
+
// Property value - Recursive expansion //
|
|
171
|
+
//////////////////////////////////////////
|
|
172
|
+
serialize(object[property])
|
|
173
|
+
})
|
|
174
|
+
buffer += '}'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module 'elliptic'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './key-util-types'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const JWK_JCS_PUB_NAME = 'jwk_jcs-pub'
|
|
2
|
+
export const JWK_JCS_PUB_PREFIX = 0xeb51
|
|
3
|
+
|
|
4
|
+
export type TKeyType = 'Ed25519' | 'Secp256k1' | 'Secp256r1' | 'X25519' | 'Bls12381G1' | 'Bls12381G2' | 'RSA'
|
|
5
|
+
|
|
6
|
+
export enum Key {
|
|
7
|
+
Ed25519 = 'Ed25519',
|
|
8
|
+
Secp256k1 = 'Secp256k1',
|
|
9
|
+
Secp256r1 = 'Secp256r1',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum JwkKeyUse {
|
|
13
|
+
Encryption = 'enc',
|
|
14
|
+
Signature = 'sig',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export enum KeyCurve {
|
|
18
|
+
Secp256k1 = 'secp256k1',
|
|
19
|
+
P_256 = 'P-256',
|
|
20
|
+
Ed25519 = 'Ed25519',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum KeyType {
|
|
24
|
+
EC = 'EC',
|
|
25
|
+
OKP = 'OKP',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SIG_KEY_ALGS = ['ES256', 'ES384', 'ES512', 'EdDSA', 'ES256K', 'Ed25519', 'Secp256k1', 'Secp256r1', 'Bls12381G1', 'Bls12381G2']
|
|
29
|
+
export const ENC_KEY_ALGS = ['X25519', 'ECDH_ES_A256KW', 'RSA_OAEP_256']
|
|
30
|
+
|
|
31
|
+
export interface JWK extends JsonWebKey {
|
|
32
|
+
x5c?: string
|
|
33
|
+
x5u?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type KeyVisibility = 'public' | 'private'
|
|
37
|
+
|
|
38
|
+
export interface X509Opts {
|
|
39
|
+
cn?: string // The certificate Common Name. Will be used as the KID for the private key. Uses alias if not provided.
|
|
40
|
+
privateKeyPEM?: string // Optional as you also need to provide it in hex format, but advisable to use it
|
|
41
|
+
certificatePEM?: string // Optional, as long as the certificate then is part of the certificateChainPEM
|
|
42
|
+
certificateChainURL?: string // Certificate chain URL. If used this is where the certificateChainPEM will be hosted/found.
|
|
43
|
+
certificateChainPEM?: string // Base64 (not url!) encoded DER certificate chain. Please provide even if certificateChainURL is used!
|
|
44
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as u8a from 'uint8arrays'
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import keyto from '@trust/keyto'
|
|
4
|
+
import { JWK, KeyVisibility } from './types'
|
|
5
|
+
|
|
6
|
+
// Based on (MIT licensed):
|
|
7
|
+
// https://github.com/hildjj/node-posh/blob/master/lib/index.js
|
|
8
|
+
export function pemCertChainTox5c(cert: string, maxDepth?: number): string[] {
|
|
9
|
+
if (!maxDepth) {
|
|
10
|
+
maxDepth = 0
|
|
11
|
+
}
|
|
12
|
+
/*
|
|
13
|
+
* Convert a PEM-encoded certificate to the version used in the x5c element
|
|
14
|
+
* of a [JSON Web Key](http://tools.ietf.org/html/draft-ietf-jose-json-web-key).
|
|
15
|
+
*
|
|
16
|
+
* `cert` PEM-encoded certificate chain
|
|
17
|
+
* `maxdepth` The maximum number of certificates to use from the chain.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const intermediate = cert
|
|
21
|
+
.replace(/-----[^\n]+\n?/gm, ',')
|
|
22
|
+
.replace(/\n/g, '')
|
|
23
|
+
.replace(/\r/g, '')
|
|
24
|
+
let x5c = intermediate.split(',').filter(function (c) {
|
|
25
|
+
return c.length > 0
|
|
26
|
+
})
|
|
27
|
+
if (maxDepth > 0) {
|
|
28
|
+
x5c = x5c.splice(0, maxDepth)
|
|
29
|
+
}
|
|
30
|
+
return x5c
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function x5cToPemCertChain(x5c: string[], maxDepth?: number): string {
|
|
34
|
+
if (!maxDepth) {
|
|
35
|
+
maxDepth = 0
|
|
36
|
+
}
|
|
37
|
+
const length = maxDepth === 0 ? x5c.length : Math.min(maxDepth, x5c.length)
|
|
38
|
+
let pem = ''
|
|
39
|
+
for (let i = 0; i < length; i++) {
|
|
40
|
+
pem += base64ToPEM(x5c[i], 'CERTIFICATE')
|
|
41
|
+
}
|
|
42
|
+
return pem
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const toKeyObject = (PEM: string, visibility: KeyVisibility = 'public') => {
|
|
46
|
+
const jwk = PEMToJwk(PEM, visibility)
|
|
47
|
+
const keyVisibility: KeyVisibility = jwk.d ? 'private' : 'public'
|
|
48
|
+
const keyHex = keyVisibility === 'private' ? privateKeyHexFromPEM(PEM) : publicKeyHexFromPEM(PEM)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
pem: hexToPEM(keyHex, visibility),
|
|
52
|
+
jwk,
|
|
53
|
+
keyHex,
|
|
54
|
+
keyType: keyVisibility,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const jwkToPEM = (jwk: JWK, visibility: KeyVisibility = 'public'): string => {
|
|
59
|
+
return keyto.from(jwk, 'jwk').toString('pem', visibility === 'public' ? 'public_pkcs8' : 'private_pkcs8')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const PEMToJwk = (pem: string, visibility: KeyVisibility = 'public'): JWK => {
|
|
63
|
+
return keyto.from(pem, 'pem').toJwk(visibility)
|
|
64
|
+
}
|
|
65
|
+
export const privateKeyHexFromPEM = (PEM: string) => {
|
|
66
|
+
return PEMToHex(PEM)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const hexKeyFromPEMBasedJwk = (jwk: JWK, visibility: KeyVisibility = 'public'): string => {
|
|
70
|
+
if (visibility === 'private') {
|
|
71
|
+
return privateKeyHexFromPEM(jwkToPEM(jwk, 'private'))
|
|
72
|
+
} else {
|
|
73
|
+
return publicKeyHexFromPEM(jwkToPEM(jwk, 'public'))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const publicKeyHexFromPEM = (PEM: string) => {
|
|
78
|
+
const hex = PEMToHex(PEM)
|
|
79
|
+
if (PEM.includes('CERTIFICATE')) {
|
|
80
|
+
throw Error('Cannot directly deduce public Key from PEM Certificate yet')
|
|
81
|
+
} else if (!PEM.includes('PRIVATE')) {
|
|
82
|
+
return hex
|
|
83
|
+
}
|
|
84
|
+
const publicJwk = PEMToJwk(PEM, 'public')
|
|
85
|
+
const publicPEM = jwkToPEM(publicJwk, 'public')
|
|
86
|
+
return PEMToHex(publicPEM)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const PEMToHex = (PEM: string, headerKey?: string): string => {
|
|
90
|
+
if (PEM.indexOf('-----BEGIN ') == -1) {
|
|
91
|
+
throw Error(`PEM header not found: ${headerKey}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let strippedPem: string
|
|
95
|
+
if (headerKey) {
|
|
96
|
+
strippedPem = PEM.replace(new RegExp('^[^]*-----BEGIN ' + headerKey + '-----'), '')
|
|
97
|
+
strippedPem = strippedPem.replace(new RegExp('-----END ' + headerKey + '-----[^]*$'), '')
|
|
98
|
+
} else {
|
|
99
|
+
strippedPem = PEM.replace(/^[^]*-----BEGIN [^-]+-----/, '')
|
|
100
|
+
strippedPem = strippedPem.replace(/-----END [^-]+-----[^]*$/, '')
|
|
101
|
+
}
|
|
102
|
+
return base64ToHex(strippedPem, 'base64pad')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Converts a base64 encoded string to hex string, removing any non-base64 characters, including newlines
|
|
107
|
+
* @param input The input in base64, with optional newlines
|
|
108
|
+
* @param inputEncoding
|
|
109
|
+
*/
|
|
110
|
+
export const base64ToHex = (input: string, inputEncoding?: 'base64pad' | 'base64urlpad') => {
|
|
111
|
+
const base64NoNewlines = input.replace(/[^0-9A-Za-z\/+=]*/g, '')
|
|
112
|
+
return u8a.toString(u8a.fromString(base64NoNewlines, inputEncoding ? inputEncoding : 'base64pad'), 'base16')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const hexToBase64 = (input: number | object | string, targetEncoding?: 'base64pad' | 'base64urlpad'): string => {
|
|
116
|
+
let hex = typeof input === 'string' ? input : input.toString(16)
|
|
117
|
+
if (hex.length % 2 === 1) {
|
|
118
|
+
hex = `0${hex}`
|
|
119
|
+
}
|
|
120
|
+
return u8a.toString(u8a.fromString(hex, 'base16'), targetEncoding ? targetEncoding : 'base64pad')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const hexToPEM = (hex: string, type: KeyVisibility): string => {
|
|
124
|
+
const base64 = hexToBase64(hex, 'base64pad')
|
|
125
|
+
const headerKey = type === 'private' ? 'RSA PRIVATE KEY' : 'PUBLIC KEY'
|
|
126
|
+
if (type === 'private') {
|
|
127
|
+
const pem = base64ToPEM(base64, headerKey)
|
|
128
|
+
try {
|
|
129
|
+
PEMToJwk(pem) // We only use it to test the private key
|
|
130
|
+
return pem
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return base64ToPEM(base64, 'PRIVATE KEY')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return base64ToPEM(base64, headerKey)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function base64ToPEM(cert: string, headerKey?: 'PUBLIC KEY' | 'RSA PRIVATE KEY' | 'PRIVATE KEY' | 'CERTIFICATE'): string {
|
|
139
|
+
const key = headerKey ?? 'CERTIFICATE'
|
|
140
|
+
const matches = cert.match(/.{1,64}/g)
|
|
141
|
+
if (!matches) {
|
|
142
|
+
throw Error('Invalid cert input value supplied')
|
|
143
|
+
}
|
|
144
|
+
return `-----BEGIN ${key}-----\n${matches.join('\n')}\n-----END ${key}-----\n`
|
|
145
|
+
}
|