@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.
@@ -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
+ }