Skip to content

Commit

Permalink
Secp256k1 support (bluesky-social#267)
Browse files Browse the repository at this point in the history
* add secp256k1 to crypto module

* integrate into plc

* use secp256k1 key in test

* use jwt alg consts & fix did resolution
  • Loading branch information
dholms authored Oct 25, 2022
1 parent d6fd69d commit e507d95
Show file tree
Hide file tree
Showing 22 changed files with 331 additions and 187 deletions.
1 change: 1 addition & 0 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"postbuild": "tsc --build tsconfig.build.json"
},
"dependencies": {
"@noble/secp256k1": "^1.7.0",
"@ucans/core": "0.11.0",
"big-integer": "^1.6.51",
"multiformats": "^9.6.4",
Expand Down
4 changes: 4 additions & 0 deletions packages/crypto/src/const.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export const P256_DID_PREFIX = new Uint8Array([0x80, 0x24])
export const SECP256K1_DID_PREFIX = new Uint8Array([0xe7, 0x01])
export const BASE58_DID_PREFIX = 'did:key:z' // z is the multibase prefix for base58btc byte encoding

export const P256_JWT_ALG = 'ES256'
export const SECP256K1_JWT_ALG = 'ES256K'
10 changes: 8 additions & 2 deletions packages/crypto/src/did.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as uint8arrays from 'uint8arrays'
import * as p256 from './p256/encoding'
import * as secp from './secp256k1/encoding'
import plugins from './plugins'
import { P256_JWT_ALG, SECP256K1_JWT_ALG } from './const'

export const DID_KEY_BASE58_PREFIX = 'did:key:z'

Expand All @@ -22,8 +24,10 @@ export const parseDidKey = (did: string): ParsedDidKey => {
throw new Error('Unsupported key type')
}
let keyBytes = prefixedBytes.slice(plugin.prefix.length)
if (plugin.jwtAlg === 'ES256') {
if (plugin.jwtAlg === P256_JWT_ALG) {
keyBytes = p256.decompressPubkey(keyBytes)
} else if (plugin.jwtAlg === SECP256K1_JWT_ALG) {
keyBytes = secp.decompressPubkey(keyBytes)
}
return {
jwtAlg: plugin.jwtAlg,
Expand All @@ -36,8 +40,10 @@ export const formatDidKey = (jwtAlg: string, keyBytes: Uint8Array): string => {
if (!plugin) {
throw new Error('Unsupported key type')
}
if (jwtAlg === 'ES256') {
if (jwtAlg === P256_JWT_ALG) {
keyBytes = p256.compressPubkey(keyBytes)
} else if (jwtAlg === SECP256K1_JWT_ALG) {
keyBytes = secp.compressPubkey(keyBytes)
}
const prefixedBytes = uint8arrays.concat([plugin.prefix, keyBytes])
return (
Expand Down
10 changes: 6 additions & 4 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export * from './aes'
export * from './const'
export * from './did'
export * from './multibase'
export * from './random'
export * from './sha'
export * from './verify'

export * from './p256/ecdh'
export * from './p256/ecdsa'
export * from './p256/keypair'
export * from './p256/plugin'
export * from './p256/operations'
export * from './p256/encoding'

export * from './secp256k1/keypair'
export * from './secp256k1/plugin'

export type { DidableKey } from '@ucans/core'
54 changes: 0 additions & 54 deletions packages/crypto/src/p256/ecdh.ts

This file was deleted.

53 changes: 0 additions & 53 deletions packages/crypto/src/p256/encoding.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,6 @@
import bigInt from 'big-integer'
import { webcrypto } from 'one-webcrypto'
import * as uint8arrays from 'uint8arrays'

import { BASE58_DID_PREFIX, P256_DID_PREFIX } from '../const'

// DID <-> Public key conversions
// ------------------------

export const pubkeyBytesFromDid = (did: string): Uint8Array => {
if (!did.startsWith(BASE58_DID_PREFIX)) {
throw new Error('Expected a base58-encoded DID formatted `did:key:z...`')
}
const didWithoutPrefix = did.slice(BASE58_DID_PREFIX.length)
const didBytes = uint8arrays.fromString(didWithoutPrefix, 'base58btc')
if (!uint8arrays.equals(P256_DID_PREFIX, didBytes.slice(0, 2))) {
throw new Error('Unsupported key method: Expected NIST P-256')
}
const compressedKeyBytes = didBytes.slice(2)
return decompressPubkey(compressedKeyBytes)
}

export const ecdsaKeyFromDid = async (did: string): Promise<CryptoKey> => {
const keyBytes = pubkeyBytesFromDid(did)
return webcrypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['verify'],
)
}

export const ecdhKeyFromDid = async (did: string): Promise<CryptoKey> => {
const keyBytes = pubkeyBytesFromDid(did)
return webcrypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'ECDH', namedCurve: 'P-256' },
true,
[],
)
}

export const didFromPubkey = async (pubkey: CryptoKey): Promise<string> => {
const buf = await webcrypto.subtle.exportKey('raw', pubkey)
const bytes = new Uint8Array(buf)
return didFromPubkeyBytes(bytes)
}

export const didFromPubkeyBytes = (pubkey: Uint8Array): string => {
const compressedKey = compressPubkey(pubkey)
const prefixedBytes = uint8arrays.concat([P256_DID_PREFIX, compressedKey])
return BASE58_DID_PREFIX + uint8arrays.toString(prefixedBytes, 'base58btc')
}

// PUBLIC KEY COMPRESSION
// ------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import * as uint8arrays from 'uint8arrays'

import * as ucan from '@ucans/core'

import * as encoding from './encoding'
import * as did from '../did'
import * as operations from './operations'
import { P256_JWT_ALG } from '../const'

export type EcdsaKeypairOptions = {
exportable: boolean
}

export class EcdsaKeypair implements ucan.DidableKey {
jwtAlg = 'ES256'
jwtAlg = P256_JWT_ALG
private publicKey: Uint8Array
private keypair: CryptoKeyPair
private exportable: boolean
Expand Down Expand Up @@ -60,7 +61,7 @@ export class EcdsaKeypair implements ucan.DidableKey {
}

did(): string {
return encoding.didFromPubkeyBytes(this.publicKey)
return did.formatDidKey(this.jwtAlg, this.publicKey)
}

async sign(msg: Uint8Array): Promise<Uint8Array> {
Expand Down
20 changes: 6 additions & 14 deletions packages/crypto/src/p256/operations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { webcrypto } from 'one-webcrypto'
import * as encoding from './encoding'
import { P256_JWT_ALG } from '../const'
import { parseDidKey } from '../did'

export const importKeypairJwk = async (
jwk: JsonWebKey,
Expand Down Expand Up @@ -29,7 +30,10 @@ export const verifyDidSig = async (
data: Uint8Array,
sig: Uint8Array,
): Promise<boolean> => {
const keyBytes = encoding.pubkeyBytesFromDid(did)
const { jwtAlg, keyBytes } = parseDidKey(did)
if (jwtAlg !== P256_JWT_ALG) {
throw new Error(`Not a P-256 did:key: ${did}`)
}
return verify(keyBytes, data, sig)
}

Expand Down Expand Up @@ -58,15 +62,3 @@ export const importEcdsaPublicKey = async (
['verify'],
)
}

export const importEcdhPublicKey = async (
keyBytes: Uint8Array,
): Promise<CryptoKey> => {
return webcrypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'ECDH', namedCurve: 'P-256' },
true,
[],
)
}
4 changes: 2 additions & 2 deletions packages/crypto/src/p256/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { DidKeyPlugin } from '@ucans/core'

import { P256_DID_PREFIX } from '../const'
import { P256_DID_PREFIX, P256_JWT_ALG } from '../const'

import * as operations from './operations'

export const p256Plugin: DidKeyPlugin = {
prefix: P256_DID_PREFIX,
jwtAlg: 'ES256',
jwtAlg: P256_JWT_ALG,
verifySignature: operations.verifyDidSig,
}

Expand Down
3 changes: 2 additions & 1 deletion packages/crypto/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import p256Plugin from './p256/plugin'
import secp256k1Plugin from './secp256k1/plugin'

export const plugins = [p256Plugin]
export const plugins = [p256Plugin, secp256k1Plugin]

export default plugins
16 changes: 16 additions & 0 deletions packages/crypto/src/secp256k1/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as secp from '@noble/secp256k1'

export const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {
const hex = secp.utils.bytesToHex(pubkeyBytes)
const point = secp.Point.fromHex(hex)
return point.toRawBytes(true)
}

export const decompressPubkey = (compressed: Uint8Array): Uint8Array => {
if (compressed.length !== 33) {
throw new Error('Expected 33 byte compress pubkey')
}
const hex = secp.utils.bytesToHex(compressed)
const point = secp.Point.fromHex(hex)
return point.toRawBytes(false)
}
64 changes: 64 additions & 0 deletions packages/crypto/src/secp256k1/keypair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as ucan from '@ucans/core'
import * as secp from '@noble/secp256k1'
import * as uint8arrays from 'uint8arrays'
import * as did from '../did'
import { SECP256K1_JWT_ALG } from '../const'

export type Secp256k1KeypairOptions = {
exportable: boolean
}

export class Secp256k1Keypair implements ucan.DidableKey {
jwtAlg = SECP256K1_JWT_ALG
private publicKey: Uint8Array

constructor(private privateKey: Uint8Array, private exportable: boolean) {
this.publicKey = secp.getPublicKey(privateKey)
}

static async create(
opts?: Partial<Secp256k1KeypairOptions>,
): Promise<Secp256k1Keypair> {
const { exportable = false } = opts || {}
const privKey = secp.utils.randomPrivateKey()
return new Secp256k1Keypair(privKey, exportable)
}

static async import(
privKey: Uint8Array | string,
opts?: Partial<Secp256k1KeypairOptions>,
): Promise<Secp256k1Keypair> {
const { exportable = false } = opts || {}
const privKeyBytes =
typeof privKey === 'string'
? uint8arrays.fromString(privKey, 'hex')
: privKey
return new Secp256k1Keypair(privKeyBytes, exportable)
}

publicKeyBytes(): Uint8Array {
return this.publicKey
}

publicKeyStr(encoding: ucan.Encodings = 'base64pad'): string {
return uint8arrays.toString(this.publicKey, encoding)
}

did(): string {
return did.formatDidKey(this.jwtAlg, this.publicKey)
}

async sign(msg: Uint8Array): Promise<Uint8Array> {
const msgHash = await secp.utils.sha256(msg)
return secp.sign(msgHash, this.privateKey)
}

async export(): Promise<Uint8Array> {
if (!this.exportable) {
throw new Error('Private key is not exportable')
}
return this.privateKey
}
}

export default Secp256k1Keypair
16 changes: 16 additions & 0 deletions packages/crypto/src/secp256k1/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as secp from '@noble/secp256k1'
import { SECP256K1_JWT_ALG } from '../const'
import { parseDidKey } from '../did'

export const verifyDidSig = async (
did: string,
data: Uint8Array,
sig: Uint8Array,
): Promise<boolean> => {
const { jwtAlg, keyBytes } = parseDidKey(did)
if (jwtAlg !== SECP256K1_JWT_ALG) {
throw new Error(`Not a secp256k1 did:key: ${did}`)
}
const msgHash = await secp.utils.sha256(data)
return secp.verify(sig, msgHash, keyBytes)
}
11 changes: 11 additions & 0 deletions packages/crypto/src/secp256k1/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DidKeyPlugin } from '@ucans/core'
import * as operations from './operations'
import { SECP256K1_DID_PREFIX, SECP256K1_JWT_ALG } from '../const'

export const secp256k1Plugin: DidKeyPlugin = {
prefix: SECP256K1_DID_PREFIX,
jwtAlg: SECP256K1_JWT_ALG,
verifySignature: operations.verifyDidSig,
}

export default secp256k1Plugin
15 changes: 15 additions & 0 deletions packages/crypto/src/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { parseDidKey } from './did'
import plugins from './plugins'

export const verifyDidSig = (
did: string,
data: Uint8Array,
sig: Uint8Array,
): Promise<boolean> => {
const parsed = parseDidKey(did)
const plugin = plugins.find((p) => p.jwtAlg === parsed.jwtAlg)
if (!plugin) {
throw new Error(`Unsupported signature alg: :${parsed.jwtAlg}`)
}
return plugin.verifySignature(did, data, sig)
}
Loading

0 comments on commit e507d95

Please sign in to comment.