Skip to content

Commit

Permalink
Cleanup @atproto/crypto (bluesky-social#1218)
Browse files Browse the repository at this point in the history
* remove webcrypto, upgrade @noble/curves, normalize p256 interface

* cleanup

* Test vectors for secp and p256 signature verification (bluesky-social#737)

Add test vectors for secp and p256 signature verification

* fix up test vectors

* add explicit test vectors for high-s signatures

* tidy json to pass verify check

---------

Co-authored-by: devin ivy <[email protected]>
  • Loading branch information
dholms and devinivy authored Jun 26, 2023
1 parent 7333253 commit 2e94c43
Show file tree
Hide file tree
Showing 23 changed files with 338 additions and 290 deletions.
6 changes: 4 additions & 2 deletions packages/aws/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"@aws-sdk/client-kms": "^3.196.0",
"@aws-sdk/client-s3": "^3.224.0",
"@aws-sdk/lib-storage": "^3.226.0",
"@noble/secp256k1": "^1.7.0",
"key-encoder": "^2.0.3"
"@noble/curves": "^1.1.0",
"key-encoder": "^2.0.3",
"multiformats": "^9.6.4",
"uint8arrays": "^4.0.4"
}
}
7 changes: 4 additions & 3 deletions packages/aws/src/kms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as aws from '@aws-sdk/client-kms'
import * as secp from '@noble/secp256k1'
import { secp256k1 as noble } from '@noble/curves/secp256k1'
import * as ui8 from 'uint8arrays'
import * as crypto from '@atproto/crypto'
import KeyEncoder from 'key-encoder'

Expand Down Expand Up @@ -35,7 +36,7 @@ export class KmsKeypair implements crypto.Keypair {
'der',
'raw',
)
const publicKey = secp.utils.hexToBytes(rawPublicKeyHex)
const publicKey = ui8.fromString(rawPublicKeyHex, 'hex')
return new KmsKeypair(client, keyId, publicKey)
}

Expand All @@ -57,7 +58,7 @@ export class KmsKeypair implements crypto.Keypair {
// we also normalize s as no more than 1/2 prime order to pass strict verification
// (prevents duplicating a signature)
// more: https://github.com/bitcoin-core/secp256k1/blob/a1102b12196ea27f44d6201de4d25926a2ae9640/include/secp256k1.h#L530-L534
const sig = secp.Signature.fromDER(res.Signature)
const sig = noble.Signature.fromDER(res.Signature)
const normalized = sig.normalizeS()
return normalized.toCompactRawBytes()
}
Expand Down
6 changes: 2 additions & 4 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@
"postpublish": "npm run update-main-to-src"
},
"dependencies": {
"@noble/secp256k1": "^1.7.0",
"big-integer": "^1.6.51",
"multiformats": "^9.6.4",
"one-webcrypto": "^1.0.3",
"@noble/curves": "^1.1.0",
"@noble/hashes": "^1.3.1",
"uint8arrays": "3.0.0"
}
}
64 changes: 0 additions & 64 deletions packages/crypto/src/aes.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './aes'
export * from './const'
export * from './did'
export * from './multibase'
Expand Down
27 changes: 27 additions & 0 deletions packages/crypto/src/multibase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as uint8arrays from 'uint8arrays'
import { SupportedEncodings } from 'uint8arrays/to-string'

export const multibaseToBytes = (mb: string): Uint8Array => {
const base = mb[0]
Expand All @@ -24,3 +25,29 @@ export const multibaseToBytes = (mb: string): Uint8Array => {
throw new Error(`Unsupported multibase: :${mb}`)
}
}

export const bytesToMultibase = (
mb: Uint8Array,
encoding: SupportedEncodings,
): string => {
switch (encoding) {
case 'base16':
return 'f' + uint8arrays.toString(mb, encoding)
case 'base16upper':
return 'F' + uint8arrays.toString(mb, encoding)
case 'base32':
return 'b' + uint8arrays.toString(mb, encoding)
case 'base32upper':
return 'B' + uint8arrays.toString(mb, encoding)
case 'base58btc':
return 'z' + uint8arrays.toString(mb, encoding)
case 'base64':
return 'm' + uint8arrays.toString(mb, encoding)
case 'base64url':
return 'u' + uint8arrays.toString(mb, encoding)
case 'base64urlpad':
return 'U' + uint8arrays.toString(mb, encoding)
default:
throw new Error(`Unsupported multibase: :${mb}`)
}
}
77 changes: 5 additions & 72 deletions packages/crypto/src/p256/encoding.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,14 @@
import bigInt from 'big-integer'
import * as uint8arrays from 'uint8arrays'
import { p256 } from '@noble/curves/p256'

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

// Compression & Decompression algos from:
// https://stackoverflow.com/questions/48521840/biginteger-to-a-uint8array-of-bytes

// Public key compression for NIST P-256
export const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {
if (pubkeyBytes.length !== 65) {
throw new Error('Expected 65 byte pubkey')
} else if (pubkeyBytes[0] !== 0x04) {
throw new Error('Expected first byte to be 0x04')
}
// first byte is a prefix
const x = pubkeyBytes.slice(1, 33)
const y = pubkeyBytes.slice(33, 65)
const out = new Uint8Array(x.length + 1)

out[0] = 2 + (y[y.length - 1] & 1)
out.set(x, 1)

return out
const point = p256.ProjectivePoint.fromHex(pubkeyBytes)
return point.toRawBytes(true)
}

// Public key decompression for NIST P-256
export const decompressPubkey = (compressed: Uint8Array): Uint8Array => {
if (compressed.length !== 33) {
throw new Error('Expected 33 byte compress pubkey')
} else if (compressed[0] !== 0x02 && compressed[0] !== 0x03) {
throw new Error('Expected first byte to be 0x02 or 0x03')
}
// Consts for P256 curve
const two = bigInt(2)
// 115792089210356248762697446949407573530086143415290314195533631308867097853951
const prime = two
.pow(256)
.subtract(two.pow(224))
.add(two.pow(192))
.add(two.pow(96))
.subtract(1)
const b = bigInt(
'41058363725152142129326129780047268409114441015993725554835256314039467401291',
)

// Pre-computed value, or literal
const pIdent = prime.add(1).divide(4) // 28948022302589062190674361737351893382521535853822578548883407827216774463488

// This value must be 2 or 3. 4 indicates an uncompressed key, and anything else is invalid.
const signY = bigInt(compressed[0] - 2)
const x = compressed.slice(1)
const xBig = bigInt(uint8arrays.toString(x, 'base10'))

// y^2 = x^3 - 3x + b
const maybeY = xBig
.pow(3)
.subtract(xBig.multiply(3))
.add(b)
.modPow(pIdent, prime)

let yBig
// If the parity matches, we found our root, otherwise it's the other root
if (maybeY.mod(2).equals(signY)) {
yBig = maybeY
} else {
// y = prime - y
yBig = prime.subtract(maybeY)
}
const y = uint8arrays.fromString(yBig.toString(10), 'base10')

// left-pad for smaller than 32 byte y
const offset = 32 - y.length
const yPadded = new Uint8Array(32)
yPadded.set(y, offset)

// concat coords & prepend P-256 prefix
const publicKey = uint8arrays.concat([[0x04], x, yPadded])
return publicKey
const point = p256.ProjectivePoint.fromHex(compressed)
return point.toRawBytes(false)
}
66 changes: 25 additions & 41 deletions packages/crypto/src/p256/keypair.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,41 @@
import { webcrypto } from 'one-webcrypto'
import { p256 } from '@noble/curves/p256'
import { sha256 } from '@noble/hashes/sha256'
import * as uint8arrays from 'uint8arrays'
import { SupportedEncodings } from 'uint8arrays/util/bases'
import * as did from '../did'
import * as operations from './operations'
import { P256_JWT_ALG } from '../const'
import { Keypair } from '../types'

export type EcdsaKeypairOptions = {
export type P256KeypairOptions = {
exportable: boolean
}

export class EcdsaKeypair implements Keypair {
export class P256Keypair implements Keypair {
jwtAlg = P256_JWT_ALG
private publicKey: Uint8Array
private keypair: CryptoKeyPair
private exportable: boolean

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

static async create(
opts?: Partial<EcdsaKeypairOptions>,
): Promise<EcdsaKeypair> {
opts?: Partial<P256KeypairOptions>,
): Promise<P256Keypair> {
const { exportable = false } = opts || {}
const keypair = await webcrypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
exportable,
['sign', 'verify'],
)
const pubkeyBuf = await webcrypto.subtle.exportKey('raw', keypair.publicKey)
const pubkeyBytes = new Uint8Array(pubkeyBuf)
return new EcdsaKeypair(keypair, pubkeyBytes, exportable)
const privKey = p256.utils.randomPrivateKey()
return new P256Keypair(privKey, exportable)
}

static async import(
jwk: JsonWebKey,
opts?: Partial<EcdsaKeypairOptions>,
): Promise<EcdsaKeypair> {
privKey: Uint8Array | string,
opts?: Partial<P256KeypairOptions>,
): Promise<P256Keypair> {
const { exportable = false } = opts || {}
const keypair = await operations.importKeypairJwk(jwk, exportable)
const pubkeyBuf = await webcrypto.subtle.exportKey('raw', keypair.publicKey)
const pubkeyBytes = new Uint8Array(pubkeyBuf)
return new EcdsaKeypair(keypair, pubkeyBytes, exportable)
const privKeyBytes =
typeof privKey === 'string'
? uint8arrays.fromString(privKey, 'hex')
: privKey
return new P256Keypair(privKeyBytes, exportable)
}

publicKeyBytes(): Uint8Array {
Expand All @@ -64,21 +51,18 @@ export class EcdsaKeypair implements Keypair {
}

async sign(msg: Uint8Array): Promise<Uint8Array> {
const buf = await webcrypto.subtle.sign(
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
this.keypair.privateKey,
new Uint8Array(msg),
)
return new Uint8Array(buf)
const msgHash = await sha256(msg)
// return raw 64 byte sig not DER-encoded
const sig = await p256.sign(msgHash, this.privateKey, { lowS: true })
return sig.toCompactRawBytes()
}

async export(): Promise<JsonWebKey> {
async export(): Promise<Uint8Array> {
if (!this.exportable) {
throw new Error('Private key is not exportable')
}
const jwk = await webcrypto.subtle.exportKey('jwk', this.keypair.privateKey)
return jwk
return this.privateKey
}
}

export default EcdsaKeypair
export default P256Keypair
Loading

0 comments on commit 2e94c43

Please sign in to comment.