Skip to content

Commit

Permalink
Support WebCrypto ECDSA (#57)
Browse files Browse the repository at this point in the history
* Add ecdsa support

* Add new test cases

* Rollback formatting for types file
  • Loading branch information
appcypher authored Mar 22, 2022
1 parent 979ea39 commit 10ec2ef
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 41 deletions.
76 changes: 76 additions & 0 deletions src/crypto/ecdsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { webcrypto } from "one-webcrypto"
import { NamedCurve, KeyType } from "../types"

export const ALG = "ECDSA"
export const DEFAULT_CURVE = "P-256"
export const DEFAULT_HASH_ALG = "SHA-256"

export const generateKeypair = async (
namedCurve: NamedCurve = DEFAULT_CURVE
): Promise<CryptoKeyPair> => {
return await webcrypto.subtle.generateKey(
{
name: ALG,
namedCurve,
},
false,
["sign", "verify"]
)
}

export const exportKey = async (key: CryptoKey): Promise<Uint8Array> => {
const buf = await webcrypto.subtle.exportKey("spki", key)
return new Uint8Array(buf)
}

export const importKey = async (
key: Uint8Array,
namedCurve: NamedCurve
): Promise<CryptoKey> => {
return await webcrypto.subtle.importKey(
"spki",
key.buffer,
{ name: ALG, namedCurve },
true,
["verify"]
)
}

export const sign = async (
msg: Uint8Array,
privateKey: CryptoKey
): Promise<Uint8Array> => {
const buf = await webcrypto.subtle.sign(
{ name: ALG, hash: { name: DEFAULT_HASH_ALG } },
privateKey,
msg.buffer
)
return new Uint8Array(buf)
}

export const verify = async (
msg: Uint8Array,
sig: Uint8Array,
pubKey: Uint8Array,
namedCurve: NamedCurve
): Promise<boolean> => {
return await webcrypto.subtle.verify(
{ name: ALG, hash: { name: DEFAULT_HASH_ALG } },
await importKey(pubKey, namedCurve),
sig.buffer,
msg.buffer
)
}

export const toKeyType = (namedCurve: NamedCurve): KeyType => {
switch (namedCurve) {
case "P-256":
return "p256"
case "P-384":
return "p384"
case "P-521":
return "p521"
default:
throw new Error(`Unsupported namedCurve: ${namedCurve}`)
}
}
1 change: 1 addition & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * as rsa from "./rsa"
export * as ecdsa from "./ecdsa"
34 changes: 17 additions & 17 deletions src/crypto/rsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ export const verify = async (msg: Uint8Array, sig: Uint8Array, pubKey: Uint8Arra
/**
* The ASN.1 DER encoded header that needs to be added to an
* ASN.1 DER encoded RSAPublicKey to make it a SubjectPublicKeyInfo.
*
*
* This byte sequence is always the same.
*
*
* A human-readable version of this as part of a dumpasn1 dump:
*
*
* SEQUENCE {
* OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
* NULL
* }
*
*
* See https://github.com/ucan-wg/ts-ucan/issues/30
*/
const SPKI_PARAMS_ENCODED = new Uint8Array([48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0])
Expand Down Expand Up @@ -152,17 +152,17 @@ function asn1Skip(input: Uint8Array, expectedTag: Uint8Array, position: number):
}

function asn1Into(input: Uint8Array, expectedTag: Uint8Array, position: number): { position: number; length: number } {
// tag
const lengthPos = position + expectedTag.length
const actualTag = input.subarray(position, lengthPos)
if (!uint8arrays.equals(actualTag, expectedTag)) {
throw new Error(`ASN parsing error: Expected tag 0x${uint8arrays.toString(expectedTag, "hex")} at position ${position}, but got ${uint8arrays.toString(actualTag, "hex")}.`)
}
// length
const length = asn1DERLengthDecodeWithConsumed(input.subarray(lengthPos/*, we don't know the end */))
const contentPos = position + 1 + length.consumed
// content
return { position: contentPos, length: length.number }
// tag
const lengthPos = position + expectedTag.length
const actualTag = input.subarray(position, lengthPos)
if (!uint8arrays.equals(actualTag, expectedTag)) {
throw new Error(`ASN parsing error: Expected tag 0x${uint8arrays.toString(expectedTag, "hex")} at position ${position}, but got ${uint8arrays.toString(actualTag, "hex")}.`)
}

// length
const length = asn1DERLengthDecodeWithConsumed(input.subarray(lengthPos/*, we don't know the end */))
const contentPos = position + 1 + length.consumed

// content
return { position: contentPos, length: length.number }
}
78 changes: 59 additions & 19 deletions src/did/prefix.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as uint8arrays from "uint8arrays"
import { KeyType } from "../types"


// Each prefix is varint-encoded. So e.g. 0x1205 gets varint-encoded to 0x8524
// The varint encoding is described here: https://github.com/multiformats/unsigned-varint
// These varints are encoded big-endian in 7-bit pieces.
Expand All @@ -10,63 +9,101 @@ import { KeyType } from "../types"
// The next 7 bits encode as 0x24 (instead of 0x12) => 0x8524

/** https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L94 */
export const EDWARDS_DID_PREFIX = new Uint8Array([ 0xed, 0x01 ])
export const EDWARDS_DID_PREFIX = new Uint8Array([0xed, 0x01])
/** https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L91 */
export const BLS_DID_PREFIX = new Uint8Array([ 0xea, 0x01 ])
export const BLS_DID_PREFIX = new Uint8Array([0xea, 0x01])
/** https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L141 */
export const P256_DID_PREFIX = new Uint8Array([0x80, 0x24])
/** https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L142 */
export const P384_DID_PREFIX = new Uint8Array([0x81, 0x24])
/** https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L143 */
export const P521_DID_PREFIX = new Uint8Array([0x82, 0x24])
/** https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L146 */
export const RSA_DID_PREFIX = new Uint8Array([ 0x85, 0x24 ])
export const RSA_DID_PREFIX = new Uint8Array([0x85, 0x24])
/** Old RSA DID prefix, used pre-standardisation */
export const RSA_DID_PREFIX_OLD = new Uint8Array([ 0x00, 0xf5, 0x02 ])
export const RSA_DID_PREFIX_OLD = new Uint8Array([0x00, 0xf5, 0x02])

export const BASE58_DID_PREFIX = "did:key:z" // z is the multibase prefix for base58btc byte encoding


/**
* Magic bytes.
*/
export function magicBytes(keyType: KeyType): Uint8Array | null {
switch (keyType) {
case "ed25519": return EDWARDS_DID_PREFIX
case "rsa": return RSA_DID_PREFIX
case "bls12-381": return BLS_DID_PREFIX
default: return null
case "ed25519":
return EDWARDS_DID_PREFIX
case "p256":
return P256_DID_PREFIX
case "p384":
return P384_DID_PREFIX
case "p521":
return P521_DID_PREFIX
case "rsa":
return RSA_DID_PREFIX
case "bls12-381":
return BLS_DID_PREFIX
default:
return null
}
}

/**
* Parse magic bytes on prefixed key-bytes
* to determine cryptosystem & the unprefixed key-bytes.
*/
export const parseMagicBytes = (prefixedKey: Uint8Array): {
export const parseMagicBytes = (
prefixedKey: Uint8Array
): {
keyBytes: Uint8Array
type: KeyType
} => {
// RSA
if (hasPrefix(prefixedKey, RSA_DID_PREFIX)) {
return {
keyBytes: prefixedKey.slice(RSA_DID_PREFIX.byteLength),
type: "rsa"
type: "rsa",
}

// RSA OLD
// RSA OLD
} else if (hasPrefix(prefixedKey, RSA_DID_PREFIX_OLD)) {
return {
keyBytes: prefixedKey.slice(RSA_DID_PREFIX_OLD.byteLength),
type: "rsa"
type: "rsa",
}

// EC P-256
} else if (hasPrefix(prefixedKey, P256_DID_PREFIX)) {
return {
keyBytes: prefixedKey.slice(P256_DID_PREFIX.byteLength),
type: "p256",
}

// EC P-384
} else if (hasPrefix(prefixedKey, P384_DID_PREFIX)) {
return {
keyBytes: prefixedKey.slice(P384_DID_PREFIX.byteLength),
type: "p384",
}

// EC P-521
} else if (hasPrefix(prefixedKey, P521_DID_PREFIX)) {
return {
keyBytes: prefixedKey.slice(P521_DID_PREFIX.byteLength),
type: "p521",
}

// EDWARDS
// EDWARDS
} else if (hasPrefix(prefixedKey, EDWARDS_DID_PREFIX)) {
return {
keyBytes: prefixedKey.slice(EDWARDS_DID_PREFIX.byteLength),
type: "ed25519"
type: "ed25519",
}

// BLS
// BLS
} else if (hasPrefix(prefixedKey, BLS_DID_PREFIX)) {
return {
keyBytes: prefixedKey.slice(BLS_DID_PREFIX.byteLength),
type: "bls12-381"
type: "bls12-381",
}
}

Expand All @@ -76,6 +113,9 @@ export const parseMagicBytes = (prefixedKey: Uint8Array): {
/**
* Determines if a Uint8Array has a given indeterminate length-prefix.
*/
export const hasPrefix = (prefixedKey: Uint8Array, prefix: Uint8Array): boolean => {
export const hasPrefix = (
prefixedKey: Uint8Array,
prefix: Uint8Array
): boolean => {
return uint8arrays.equals(prefix, prefixedKey.subarray(0, prefix.byteLength))
}
11 changes: 11 additions & 0 deletions src/did/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as uint8arrays from "uint8arrays"
import nacl from "tweetnacl"

import * as rsa from "../crypto/rsa"
import * as ecdsa from "../crypto/ecdsa"

import { didToPublicKeyBytes } from "./transformers"


Expand All @@ -20,6 +22,15 @@ export async function verifySignature(data: Uint8Array, signature: Uint8Array, d
case "rsa":
return await rsa.verify(data, signature, publicKey)

case "p256":
return await ecdsa.verify(data, signature, publicKey, "P-256")

case "p384":
return await ecdsa.verify(data, signature, publicKey, "P-384")

case "p521":
return await ecdsa.verify(data, signature, publicKey, "P-521")

default: return false
}

Expand Down
56 changes: 56 additions & 0 deletions src/keypair/ecdsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { webcrypto } from "one-webcrypto"
import * as uint8arrays from "uint8arrays"
import * as ecdsa from "../crypto/ecdsa"
import {
AvailableCryptoKeyPair,
Encodings,
isAvailableCryptoKeyPair,
NamedCurve,
} from "../types"
import BaseKeypair from "./base"

export class EcdsaKeypair extends BaseKeypair {
private keypair: AvailableCryptoKeyPair

constructor(
keypair: AvailableCryptoKeyPair,
publicKey: Uint8Array,
namedCurve: NamedCurve,
exportable: boolean
) {
super(publicKey, ecdsa.toKeyType(namedCurve), exportable)
this.keypair = keypair
}

static async create(params?: {
namedCurve?: NamedCurve
exportable?: boolean
}): Promise<EcdsaKeypair> {
const { namedCurve = "P-256", exportable = false } = params || {}
const keypair = await ecdsa.generateKeypair(namedCurve)

if (!isAvailableCryptoKeyPair(keypair)) {
throw new Error(`Couldn't generate valid keypair`)
}

const publicKey = await ecdsa.exportKey(keypair.publicKey)
return new EcdsaKeypair(keypair, publicKey, namedCurve, exportable)
}

async sign(msg: Uint8Array): Promise<Uint8Array> {
return await ecdsa.sign(msg, this.keypair.privateKey)
}

async export(format: Encodings = "base64pad"): Promise<string> {
if (!this.exportable) {
throw new Error("Key is not exportable")
}
const arrayBuffer = await webcrypto.subtle.exportKey(
"pkcs8",
this.keypair.privateKey
)
return uint8arrays.toString(new Uint8Array(arrayBuffer), format)
}
}

export default EcdsaKeypair
1 change: 1 addition & 0 deletions src/keypair/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./ed25519"
export * from "./ecdsa"
export * from "./rsa"
export * from "./base"
2 changes: 1 addition & 1 deletion src/keypair/rsa.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { webcrypto } from "one-webcrypto"
import * as uint8arrays from "uint8arrays"

import * as rsa from "../crypto/rsa"
import * as rsa from "../crypto/rsa"
import BaseKeypair from "./base"
import { Encodings, AvailableCryptoKeyPair, isAvailableCryptoKeyPair } from "../types"

Expand Down
12 changes: 10 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SupportedEncodings } from "uint8arrays/util/bases"
import * as util from "./util"


export type Encodings = SupportedEncodings

export interface Keypair {
Expand All @@ -25,7 +24,16 @@ export interface ExportableKey {
export: (format?: Encodings) => Promise<string>
}

export type KeyType = "rsa" | "ed25519" | "bls12-381"
export type KeyType =
| "rsa"
| "p256"
| "p384"
| "p521"
| "ed25519"
| "bls12-381"

// https://developer.mozilla.org/en-US/docs/Web/API/EcKeyGenParams
export type NamedCurve = "P-256" | "P-384" | "P-521"

export type Fact = Record<string, unknown>

Expand Down
Loading

0 comments on commit 10ec2ef

Please sign in to comment.