Skip to content
This repository has been archived by the owner on Jul 21, 2023. It is now read-only.

fix: use node crypto for ed25519 signing and verification #289

Merged
merged 5 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 25 additions & 26 deletions benchmark/ed25519/index.cjs → benchmark/ed25519/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
/* eslint-disable no-console */
// @ts-expect-error types are missing
const forge = require('node-forge/lib/forge')
const Benchmark = require('benchmark')
const native = require('ed25519')
const noble = require('@noble/ed25519')
import forge from 'node-forge/lib/forge.js'
import Benchmark from 'benchmark'
import native from 'ed25519'
import * as noble from '@noble/ed25519'
import 'node-forge/lib/ed25519.js'
import stable from '@stablelib/ed25519'
import supercopWasm from 'supercop.wasm'
import ed25519WasmPro from 'ed25519-wasm-pro'
import * as libp2pCrypto from '../../dist/src/index.js'

const { randomBytes } = noble.utils
const { subtle } = require('crypto').webcrypto
require('node-forge/lib/ed25519')
const stable = require('@stablelib/ed25519')
const supercopWasm = require('supercop.wasm')
const ed25519WasmPro = require('ed25519-wasm-pro')

const suite = new Benchmark.Suite('ed25519 implementations')

suite.add('@libp2p/crypto', async (d) => {
const message = Buffer.from('hello world ' + Math.random())

const key = await libp2pCrypto.keys.generateKeyPair('Ed25519')

const signature = await key.sign(message)
const res = await key.public.verify(message, signature)

if (!res) {
throw new Error('could not verify @libp2p/crypto signature')
}

d.resolve()
}, { defer: true })

suite.add('@noble/ed25519', async (d) => {
const message = Buffer.from('hello world ' + Math.random())
const privateKey = noble.utils.randomPrivateKey()
Expand Down Expand Up @@ -96,23 +112,6 @@ suite.add('ed25519 (native module)', async (d) => {
d.resolve()
}, { defer: true })

suite.add('node.js web-crypto', async (d) => {
const message = Buffer.from('hello world ' + Math.random())

const key = await subtle.generateKey({
name: 'NODE-ED25519',
namedCurve: 'NODE-ED25519'
}, true, ['sign', 'verify'])
const signature = await subtle.sign('NODE-ED25519', key.privateKey, message)
const res = await subtle.verify('NODE-ED25519', key.publicKey, signature, message)

if (!res) {
throw new Error('could not verify node.js signature')
}

d.resolve()
}, { defer: true })

async function main () {
await Promise.all([
new Promise((resolve) => {
Expand Down
1 change: 1 addition & 0 deletions benchmark/ed25519/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "libp2p-crypto-ed25519-benchmarks",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node .",
"compat": "node compat.js"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
"./dist/src/ciphers/aes-gcm.js": "./dist/src/ciphers/aes-gcm.browser.js",
"./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js",
"./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js",
"./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js",
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js"
}
}
63 changes: 63 additions & 0 deletions src/keys/ed25519-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as ed from '@noble/ed25519'

const PUBLIC_KEY_BYTE_LENGTH = 32
const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys
const KEYS_BYTE_LENGTH = 32

export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength }
export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }

export async function generateKey () {
// the actual private key (32 bytes)
const privateKeyRaw = ed.utils.randomPrivateKey()
const publicKey = await ed.getPublicKey(privateKeyRaw)

// concatenated the public key to the private key
const privateKey = concatKeys(privateKeyRaw, publicKey)

return {
privateKey,
publicKey
}
}

/**
* Generate keypair from a 32 byte uint8array
*/
export async function generateKeyFromSeed (seed: Uint8Array) {
if (seed.length !== KEYS_BYTE_LENGTH) {
throw new TypeError('"seed" must be 32 bytes in length.')
} else if (!(seed instanceof Uint8Array)) {
throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.')
}

// based on node forges algorithm, the seed is used directly as private key
const privateKeyRaw = seed
const publicKey = await ed.getPublicKey(privateKeyRaw)

const privateKey = concatKeys(privateKeyRaw, publicKey)

return {
privateKey,
publicKey
}
}

export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array) {
const privateKeyRaw = privateKey.slice(0, KEYS_BYTE_LENGTH)

return await ed.sign(msg, privateKeyRaw)
}

export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array) {
return await ed.verify(sig, msg, publicKey)
}

function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array) {
const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH)
for (let i = 0; i < KEYS_BYTE_LENGTH; i++) {
privateKey[i] = privateKeyRaw[i]
privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i]
}
return privateKey
}
96 changes: 77 additions & 19 deletions src/keys/ed25519.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import * as ed from '@noble/ed25519'
import crypto from 'crypto'
import { promisify } from 'util'
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'

const keypair = promisify(crypto.generateKeyPair)

const PUBLIC_KEY_BYTE_LENGTH = 32
const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys
const KEYS_BYTE_LENGTH = 32
const SIGNATURE_BYTE_LENGTH = 64

export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength }
export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }

function derivePublicKey (privateKey: Uint8Array) {
const hash = crypto.createHash('sha512')
hash.update(privateKey)
return hash.digest().subarray(32)
}

export async function generateKey () {
// the actual private key (32 bytes)
const privateKeyRaw = ed.utils.randomPrivateKey()
const publicKey = await ed.getPublicKey(privateKeyRaw)
const key = await keypair('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'jwk' },
privateKeyEncoding: { type: 'pkcs8', format: 'jwk' }
})

// concatenated the public key to the private key
const privateKey = concatKeys(privateKeyRaw, publicKey)
// @ts-expect-error node types are missing jwk as a format
const privateKeyRaw = uint8arrayFromString(key.privateKey.d, 'base64url')
// @ts-expect-error node types are missing jwk as a format
const publicKeyRaw = uint8arrayFromString(key.privateKey.x, 'base64url')

return {
privateKey,
publicKey
privateKey: concatKeys(privateKeyRaw, publicKeyRaw),
publicKey: publicKeyRaw
}
}

Expand All @@ -32,25 +47,68 @@ export async function generateKeyFromSeed (seed: Uint8Array) {
}

// based on node forges algorithm, the seed is used directly as private key
const privateKeyRaw = seed
const publicKey = await ed.getPublicKey(privateKeyRaw)

const privateKey = concatKeys(privateKeyRaw, publicKey)
const publicKeyRaw = derivePublicKey(seed)

return {
privateKey,
publicKey
privateKey: concatKeys(seed, publicKeyRaw),
publicKey: publicKeyRaw
}
}

export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array) {
const privateKeyRaw = privateKey.slice(0, KEYS_BYTE_LENGTH)
export async function hashAndSign (key: Uint8Array, msg: Uint8Array) {
if (!(key instanceof Uint8Array)) {
throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.')
}

let privateKey: Uint8Array
let publicKey: Uint8Array

if (key.byteLength === PRIVATE_KEY_BYTE_LENGTH) {
privateKey = key.subarray(0, 32)
publicKey = key.slice(32)
mpetrunic marked this conversation as resolved.
Show resolved Hide resolved
} else if (key.byteLength === KEYS_BYTE_LENGTH) {
privateKey = key.slice(0, 32)
mpetrunic marked this conversation as resolved.
Show resolved Hide resolved
publicKey = derivePublicKey(privateKey)
} else {
throw new TypeError('"key" must be 64 or 32 bytes in length.')
}

const obj = crypto.createPrivateKey({
format: 'jwk',
key: {
crv: 'Ed25519',
d: uint8arrayToString(privateKey, 'base64url'),
x: uint8arrayToString(publicKey, 'base64url'),
kty: 'OKP'
}
})

return await ed.sign(msg, privateKeyRaw)
return crypto.sign(null, msg, obj)
}

export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array) {
return await ed.verify(sig, msg, publicKey)
export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array) {
if (key.byteLength !== PUBLIC_KEY_BYTE_LENGTH) {
throw new TypeError('"key" must be 32 bytes in length.')
} else if (!(key instanceof Uint8Array)) {
throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.')
}

if (sig.byteLength !== SIGNATURE_BYTE_LENGTH) {
throw new TypeError('"sig" must be 64 bytes in length.')
} else if (!(sig instanceof Uint8Array)) {
throw new TypeError('"sig" must be a node.js Buffer, or Uint8Array.')
}

const obj = crypto.createPublicKey({
format: 'jwk',
key: {
crv: 'Ed25519',
x: uint8arrayToString(key, 'base64url'),
kty: 'OKP'
}
})

return crypto.verify(null, msg, obj, sig)
}

function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array) {
Expand Down