Skip to content

Commit 0b9090a

Browse files
authored
feat: add functions to convert PrivateKey to CryptoKeyPair (#3061)
Adds `privateKeyToCryptoKeyPair` and `privateKeyFromCryptoKeyPair` functions to convert libp2p ECDSA/RSA private keys to WebCrypto `CryptoKeyPair` objects and back.
1 parent 78cd7d5 commit 0b9090a

File tree

7 files changed

+105
-27
lines changed

7 files changed

+105
-27
lines changed

packages/crypto/src/keys/ecdsa/ecdsa.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,16 @@ export class ECDSAPrivateKey implements ECDSAPrivateKeyInterface {
5757
public readonly publicKey: ECDSAPublicKey
5858
private _raw?: Uint8Array
5959

60-
constructor (jwk: JsonWebKey, publicKey: JsonWebKey) {
60+
constructor (jwk: JsonWebKey) {
6161
this.jwk = jwk
62-
this.publicKey = new ECDSAPublicKey(publicKey)
62+
this.publicKey = new ECDSAPublicKey({
63+
crv: jwk.crv,
64+
ext: jwk.ext,
65+
key_ops: ['verify'],
66+
kty: 'EC',
67+
x: jwk.x,
68+
y: jwk.y
69+
})
6370
}
6471

6572
get raw (): Uint8Array {

packages/crypto/src/keys/ecdsa/utils.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,6 @@ export function pkiMessageToECDSAPrivateKey (message: any): ECDSAPrivateKey {
6161
d,
6262
x,
6363
y
64-
}, {
65-
...P_256_KEY_JWK,
66-
key_ops: ['verify'],
67-
x,
68-
y
6964
})
7065
}
7166

@@ -79,11 +74,6 @@ export function pkiMessageToECDSAPrivateKey (message: any): ECDSAPrivateKey {
7974
d,
8075
x,
8176
y
82-
}, {
83-
...P_384_KEY_JWK,
84-
key_ops: ['verify'],
85-
x,
86-
y
8777
})
8878
}
8979

@@ -97,11 +87,6 @@ export function pkiMessageToECDSAPrivateKey (message: any): ECDSAPrivateKey {
9787
d,
9888
x,
9989
y
100-
}, {
101-
...P_521_KEY_JWK,
102-
key_ops: ['verify'],
103-
x,
104-
y
10590
})
10691
}
10792

@@ -215,7 +200,7 @@ function getOID (curve?: string): Uint8Array {
215200
export async function generateECDSAKeyPair (curve: Curve = 'P-256'): Promise<ECDSAPrivateKey> {
216201
const key = await generateECDSAKey(curve)
217202

218-
return new ECDSAPrivateKeyClass(key.privateKey, key.publicKey)
203+
return new ECDSAPrivateKeyClass(key.privateKey)
219204
}
220205

221206
export function ensureECDSAKey (key: Uint8Array, length: number): Uint8Array {

packages/crypto/src/keys/index.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
*/
1010

1111
import { InvalidParametersError, UnsupportedKeyTypeError } from '@libp2p/interface'
12+
import { ECDSAPrivateKey as ECDSAPrivateKeyClass } from './ecdsa/ecdsa.js'
1213
import { ECDSA_P_256_OID, ECDSA_P_384_OID, ECDSA_P_521_OID } from './ecdsa/index.js'
1314
import { generateECDSAKeyPair, pkiMessageToECDSAPrivateKey, pkiMessageToECDSAPublicKey, unmarshalECDSAPrivateKey, unmarshalECDSAPublicKey } from './ecdsa/utils.js'
1415
import { privateKeyLength as ed25519PrivateKeyLength, publicKeyLength as ed25519PublicKeyLength } from './ed25519/index.js'
1516
import { generateEd25519KeyPair, generateEd25519KeyPairFromSeed, unmarshalEd25519PrivateKey, unmarshalEd25519PublicKey } from './ed25519/utils.js'
1617
import * as pb from './keys.js'
1718
import { decodeDer } from './rsa/der.js'
1819
import { RSAES_PKCS1_V1_5_OID } from './rsa/index.js'
19-
import { pkcs1ToRSAPrivateKey, pkixToRSAPublicKey, generateRSAKeyPair, pkcs1MessageToRSAPrivateKey, pkixMessageToRSAPublicKey } from './rsa/utils.js'
20+
import { pkcs1ToRSAPrivateKey, pkixToRSAPublicKey, generateRSAKeyPair, pkcs1MessageToRSAPrivateKey, pkixMessageToRSAPublicKey, jwkToRSAPrivateKey } from './rsa/utils.js'
2021
import { privateKeyLength as secp256k1PrivateKeyLength, publicKeyLength as secp256k1PublicKeyLength } from './secp256k1/index.js'
2122
import { generateSecp256k1KeyPair, unmarshalSecp256k1PrivateKey, unmarshalSecp256k1PublicKey } from './secp256k1/utils.js'
2223
import type { Curve } from './ecdsa/index.js'
@@ -237,3 +238,55 @@ function toCurve (curve: any): Curve {
237238

238239
throw new InvalidParametersError('Unsupported curve, should be P-256, P-384 or P-521')
239240
}
241+
242+
/**
243+
* Convert a libp2p RSA or ECDSA private key to a WebCrypto CryptoKeyPair
244+
*/
245+
export async function privateKeyToCryptoKeyPair (privateKey: PrivateKey): Promise<CryptoKeyPair> {
246+
if (privateKey.type === 'RSA') {
247+
return {
248+
privateKey: await crypto.subtle.importKey('jwk', privateKey.jwk, {
249+
name: 'RSASSA-PKCS1-v1_5',
250+
hash: { name: 'SHA-256' }
251+
}, true, ['sign']),
252+
publicKey: await crypto.subtle.importKey('jwk', privateKey.publicKey.jwk, {
253+
name: 'RSASSA-PKCS1-v1_5',
254+
hash: { name: 'SHA-256' }
255+
}, true, ['verify'])
256+
}
257+
}
258+
259+
if (privateKey.type === 'ECDSA') {
260+
return {
261+
privateKey: await crypto.subtle.importKey('jwk', privateKey.jwk, {
262+
name: 'ECDSA',
263+
namedCurve: privateKey.jwk.crv ?? 'P-256'
264+
}, true, ['sign']),
265+
publicKey: await crypto.subtle.importKey('jwk', privateKey.publicKey.jwk, {
266+
name: 'ECDSA',
267+
namedCurve: privateKey.publicKey.jwk.crv ?? 'P-256'
268+
}, true, ['verify'])
269+
}
270+
}
271+
272+
throw new InvalidParametersError('Only RSA and ECDSA keys are supported')
273+
}
274+
275+
/**
276+
* Convert a RSA or ECDSA WebCrypto CryptoKeyPair to a libp2p private key
277+
*/
278+
export async function privateKeyFromCryptoKeyPair (keyPair: CryptoKeyPair): Promise<PrivateKey> {
279+
if (keyPair.privateKey.algorithm.name === 'RSASSA-PKCS1-v1_5') {
280+
const jwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
281+
282+
return jwkToRSAPrivateKey(jwk)
283+
}
284+
285+
if (keyPair.privateKey.algorithm.name === 'ECDSA') {
286+
const jwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
287+
288+
return new ECDSAPrivateKeyClass(jwk)
289+
}
290+
291+
throw new InvalidParametersError('Only RSA and ECDSA keys are supported')
292+
}

packages/crypto/test/keys/ecdsa.spec.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
66
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
77
import { randomBytes } from '../../src/index.js'
88
import { unmarshalECDSAPrivateKey, unmarshalECDSAPublicKey } from '../../src/keys/ecdsa/utils.js'
9-
import { generateKeyPair, privateKeyFromProtobuf, privateKeyFromRaw, publicKeyFromProtobuf, publicKeyFromRaw } from '../../src/keys/index.js'
9+
import { privateKeyToCryptoKeyPair, generateKeyPair, privateKeyFromProtobuf, privateKeyFromRaw, publicKeyFromProtobuf, publicKeyFromRaw, privateKeyFromCryptoKeyPair } from '../../src/keys/index.js'
1010
import { PrivateKey, PublicKey } from '../../src/keys/keys.js'
1111
import pbKeys from '../fixtures/ecdsa.js'
1212
import fixtures from '../fixtures/go-key-ed25519.js'
@@ -39,20 +39,23 @@ describe('ECDSA', function () {
3939
expect(res).to.be.be.true()
4040
})
4141

42-
it.skip('signs a list', async () => {
42+
it('signs a list', async () => {
4343
const text = new Uint8ArrayList(
4444
randomBytes(512),
4545
randomBytes(512)
4646
)
4747
const sig = await key.sign(text)
48-
49-
await expect(key.sign(text.subarray()))
50-
.to.eventually.deep.equal(sig, 'list did not have same signature as a single buffer')
48+
const sig2 = await key.sign(text.subarray())
5149

5250
await expect(key.publicKey.verify(text, sig))
5351
.to.eventually.be.true('did not verify message as list')
5452
await expect(key.publicKey.verify(text.subarray(), sig))
5553
.to.eventually.be.true('did not verify message as single buffer')
54+
55+
await expect(key.publicKey.verify(text, sig2))
56+
.to.eventually.be.true('did not verify message as list')
57+
await expect(key.publicKey.verify(text.subarray(), sig2))
58+
.to.eventually.be.true('did not verify message as single buffer')
5659
})
5760

5861
CURVES.forEach(curve => {
@@ -210,4 +213,12 @@ describe('ECDSA', function () {
210213
expect(sig).to.eql(fixtures.redundantPubKey.signature)
211214
})
212215
})
216+
217+
it('exports to CryptoKeyPair', async () => {
218+
const key = await generateKeyPair('ECDSA')
219+
const keyPair = await privateKeyToCryptoKeyPair(key)
220+
const key2 = await privateKeyFromCryptoKeyPair(keyPair)
221+
222+
expect(key.publicKey.toCID()).to.deep.equal(key2.publicKey.toCID())
223+
})
213224
})

packages/crypto/test/keys/ed25519.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Uint8ArrayList } from 'uint8arraylist'
55
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
66
import { randomBytes } from '../../src/index.js'
77
import { unmarshalEd25519PrivateKey, unmarshalEd25519PublicKey } from '../../src/keys/ed25519/utils.js'
8-
import { generateKeyPair, generateKeyPairFromSeed, privateKeyFromProtobuf, privateKeyFromRaw, publicKeyFromProtobuf, publicKeyFromRaw } from '../../src/keys/index.js'
8+
import { generateKeyPair, generateKeyPairFromSeed, privateKeyFromProtobuf, privateKeyFromRaw, publicKeyFromProtobuf, publicKeyFromRaw, privateKeyToCryptoKeyPair } from '../../src/keys/index.js'
99
import fixtures from '../fixtures/go-key-ed25519.js'
1010
import { testGarbage } from '../helpers/test-garbage-error-handling.js'
1111
import type { Ed25519PrivateKey } from '@libp2p/interface'
@@ -171,6 +171,13 @@ describe('ed25519', function () {
171171
expect(isPublicKey(key.publicKey)).to.be.true()
172172
})
173173

174+
it('fails to export to CryptoKeyPair', async () => {
175+
const key = await generateKeyPair('Ed25519')
176+
177+
await expect(privateKeyToCryptoKeyPair(key)).to.eventually.be.rejected
178+
.with.property('message', 'Only RSA and ECDSA keys are supported')
179+
})
180+
174181
describe('go interop', () => {
175182
// @ts-check
176183
it('verifies with data from go', async () => {

packages/crypto/test/keys/rsa.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { create } from 'multiformats/hashes/digest'
88
import { Uint8ArrayList } from 'uint8arraylist'
99
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
1010
import { randomBytes } from '../../src/index.js'
11-
import { generateKeyPair, privateKeyFromProtobuf, privateKeyFromRaw, privateKeyToProtobuf, publicKeyFromProtobuf, publicKeyFromRaw, publicKeyToProtobuf } from '../../src/keys/index.js'
11+
import { privateKeyFromCryptoKeyPair, generateKeyPair, privateKeyFromProtobuf, privateKeyFromRaw, privateKeyToProtobuf, publicKeyFromProtobuf, publicKeyFromRaw, publicKeyToProtobuf, privateKeyToCryptoKeyPair } from '../../src/keys/index.js'
1212
import * as pb from '../../src/keys/keys.js'
1313
import { RSAPrivateKey as RSAPrivateKeyClass, RSAPublicKey as RSAPublicKeyClass } from '../../src/keys/rsa/rsa.js'
1414
import { MAX_RSA_KEY_SIZE, jwkToPkcs1, jwkToPkix, jwkToRSAPrivateKey, pkcs1ToJwk, pkcs1ToRSAPrivateKey, pkixToJwk, pkixToRSAPublicKey } from '../../src/keys/rsa/utils.js'
@@ -279,6 +279,14 @@ describe('RSA', function () {
279279
roundTrip(RSA_KEY_8192_BITS)
280280
})
281281
})
282+
283+
it('exports to CryptoKeyPair', async () => {
284+
const key = await generateKeyPair('RSA')
285+
const keyPair = await privateKeyToCryptoKeyPair(key)
286+
const key2 = await privateKeyFromCryptoKeyPair(keyPair)
287+
288+
expect(key.publicKey.toCID()).to.deep.equal(key2.publicKey.toCID())
289+
})
282290
})
283291

284292
function bufToBn (u8: Uint8Array): bigint {

packages/crypto/test/keys/secp256k1.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { expect } from 'aegir/chai'
55
import { Uint8ArrayList } from 'uint8arraylist'
66
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
77
import { randomBytes } from '../../src/index.js'
8-
import { generateKeyPair, privateKeyFromRaw, privateKeyToProtobuf, publicKeyFromRaw, publicKeyToProtobuf } from '../../src/keys/index.js'
8+
import { generateKeyPair, privateKeyFromRaw, privateKeyToProtobuf, publicKeyFromRaw, publicKeyToProtobuf, privateKeyToCryptoKeyPair } from '../../src/keys/index.js'
99
import { KeyType, PrivateKey, PublicKey } from '../../src/keys/keys.js'
1010
import { hashAndSign, hashAndVerify } from '../../src/keys/secp256k1/index.js'
1111
import { unmarshalSecp256k1PrivateKey, unmarshalSecp256k1PublicKey, compressSecp256k1PublicKey, computeSecp256k1PublicKey, decompressSecp256k1PublicKey, generateSecp256k1PrivateKey, validateSecp256k1PrivateKey, validateSecp256k1PublicKey } from '../../src/keys/secp256k1/utils.js'
@@ -200,6 +200,13 @@ describe('crypto functions', () => {
200200
const recompressed = compressSecp256k1PublicKey(decompressed)
201201
expect(recompressed).to.equalBytes(pubKey)
202202
})
203+
204+
it('fails to export to CryptoKeyPair', async () => {
205+
const key = await generateKeyPair('secp256k1')
206+
207+
await expect(privateKeyToCryptoKeyPair(key)).to.eventually.be.rejected
208+
.with.property('message', 'Only RSA and ECDSA keys are supported')
209+
})
203210
})
204211

205212
describe('go interop', () => {

0 commit comments

Comments
 (0)