Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an exportable option to default-plugins #109

Merged
merged 11 commits into from
Mar 15, 2024
16 changes: 8 additions & 8 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,16 @@ export interface Didable {
did: () => string
}

export interface ExportableKey {
export: (format?: Encodings) => Promise<string>
export interface ExportableKey<T> {
export: () => Promise<T>
}

export interface Keypair {
jwtAlg: string
sign: (msg: Uint8Array) => Promise<Uint8Array>
}

export interface DidableKey extends Didable, Keypair {}
export interface DidableKey extends Didable, Keypair { }

// MISC

Expand All @@ -80,21 +80,21 @@ export type Encodings = SupportedEncodings


export interface IndexByAudience {
[ audienceDID: string ]: Array<{
[audienceDID: string]: Array<{
processedUcan: Ucan
capabilities: DelegationChain[]
}>
}

export interface StoreI {
add(ucan: Ucan): Promise<void>
getByAudience(audience: string): Ucan[]
findByAudience(audience: string, predicate: (ucan: Ucan) => boolean): Ucan | null
add(ucan: Ucan): Promise<void>
getByAudience(audience: string): Ucan[]
findByAudience(audience: string, predicate: (ucan: Ucan) => boolean): Ucan | null
findWithCapability(
audience: string,
requiredCapability: Capability,
requiredIssuer: string,
): Iterable<DelegationChain>
): Iterable<DelegationChain>
}

// BUILDER
Expand Down
38 changes: 35 additions & 3 deletions packages/default-plugins/src/ed25519/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import * as ed25519 from "@stablelib/ed25519"
import * as crypto from "./crypto.js"

import { DidableKey, Encodings, ExportableKey } from "@ucans/core"
import { PrivateKeyJwk } from "../types.js"


export class EdKeypair implements DidableKey, ExportableKey {
export class EdKeypair implements DidableKey, ExportableKey<PrivateKeyJwk> {

public jwtAlg = "EdDSA"

Expand Down Expand Up @@ -45,13 +46,44 @@ export class EdKeypair implements DidableKey, ExportableKey {
return ed25519.sign(this.secretKey, msg)
}

async export(format: Encodings = "base64pad"): Promise<string> {
async export(): Promise<PrivateKeyJwk> {
if (!this.exportable) {
throw new Error("Key is not exportable")
}
return uint8arrays.toString(this.secretKey, format)

/*
* EdDSA is relatively new and not supported everywhere. There's no good documentation
* within the JWK spec or parameter export to be able to reconstruct the key via parameters
* Example, there's no good documentation on parameterizing like other curves: (x, y, n, e)
*
* In an effort to remain compatible with other tooling in the space, the following article
* describes a way of encoding JWK that is at least consistent with other tooling. As our current
* libraries are only able to reconstruct a key via importing a secret key, encoding the secret
* as the `d` parameter seems to make sense and have some compatibility with other tools.
*
* [Link](https://gist.github.com/kousu/f3174af57e1fc42a0a88586b5a5ffdc9)
*
* While `kty` and `crv` are not absolutely required for this to work within the library,
* including them is an attempt to be closer to the [JWK Spec](https://datatracker.ietf.org/doc/html/rfc7517)
* since we are hand rolling this export.
*/
const jwk: PrivateKeyJwk = {
kty: "EC",
crv: "Ed25519",
d: uint8arrays.toString(this.secretKey, "base64pad"),
}
return jwk
}

static async import(jwk: PrivateKeyJwk, params?: { exportable: boolean }): Promise<EdKeypair> {
const { exportable = false } = params || {}

if (jwk.kty !== "EC" || jwk.crv !== "Ed25519") {
throw new Error("Cannot import key of type: ${jwk.kty} curve: ${jwk.crv} into ED25519 key")
}

return EdKeypair.fromSecretKey(jwk.d, { exportable })
}
}


Expand Down
24 changes: 16 additions & 8 deletions packages/default-plugins/src/p256/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ export const ALG = "ECDSA"
export const DEFAULT_CURVE = "P-256"
export const DEFAULT_HASH_ALG = "SHA-256"

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

Expand All @@ -32,10 +34,10 @@ export const importKeypairJwk = async (
namedCurve: DEFAULT_CURVE,
},
exportable,
["sign" ]
["sign"]
)
const { kty, crv, x, y} = privKeyJwk
const pubKeyJwk = { kty, crv, x, y}
const { kty, crv, x, y } = privKeyJwk
const pubKeyJwk = { kty, crv, x, y }
const publicKey = await webcrypto.subtle.importKey(
"jwk",
pubKeyJwk,
Expand All @@ -44,11 +46,17 @@ export const importKeypairJwk = async (
namedCurve: DEFAULT_CURVE,
},
true,
[ "verify" ]
["verify"]
)
return { privateKey, publicKey }
}

export const exportPrivateKeyJwk = async (
keyPair: AvailableCryptoKeyPair
): Promise<PrivateKeyJwk> => {
return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey) as PrivateKeyJwk
}

export const exportKey = async (key: CryptoKey): Promise<Uint8Array> => {
const buf = await webcrypto.subtle.exportKey("raw", key)
return new Uint8Array(buf)
Expand All @@ -62,7 +70,7 @@ export const importKey = async (
key,
{ name: ALG, namedCurve: DEFAULT_CURVE },
true,
[ "verify" ]
["verify"]
)
}

Expand Down
37 changes: 21 additions & 16 deletions packages/default-plugins/src/p256/keypair.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { webcrypto } from "one-webcrypto"
import * as uint8arrays from "uint8arrays"
import { DidableKey, Encodings, ExportableKey } from "@ucans/core"
import { DidableKey, ExportableKey } from "@ucans/core"

import * as crypto from "./crypto.js"
import {
Expand All @@ -10,7 +8,7 @@ import {
} from "../types.js"


export class EcdsaKeypair implements DidableKey, ExportableKey {
export class EcdsaKeypair implements DidableKey, ExportableKey<PrivateKeyJwk> {

public jwtAlg = "ES256"

Expand All @@ -32,7 +30,7 @@ export class EcdsaKeypair implements DidableKey, ExportableKey {
exportable?: boolean
}): Promise<EcdsaKeypair> {
const { exportable = false } = params || {}
const keypair = await crypto.generateKeypair()
const keypair = await crypto.generateKeypair(exportable)

if (!isAvailableCryptoKeyPair(keypair)) {
throw new Error(`Couldn't generate valid keypair`)
Expand All @@ -47,12 +45,12 @@ export class EcdsaKeypair implements DidableKey, ExportableKey {
params?: {
exportable?: boolean
}): Promise<EcdsaKeypair> {
const { exportable = false } = params || {}
const keypair = await crypto.importKeypairJwk(jwk, exportable)
const { exportable = false } = params || {}
const keypair = await crypto.importKeypairJwk(jwk, exportable)

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

const publicKey = await crypto.exportKey(keypair.publicKey)
return new EcdsaKeypair(keypair, publicKey, exportable)
Expand All @@ -66,15 +64,22 @@ export class EcdsaKeypair implements DidableKey, ExportableKey {
return await crypto.sign(msg, this.keypair.privateKey)
}

async export(format: Encodings = "base64pad"): Promise<string> {
async export(): Promise<PrivateKeyJwk> {
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)
return await crypto.exportPrivateKeyJwk(this.keypair)
}

/**
* Convenience function on the Keypair class to allow for keys to be exported / persisted.
* This is most useful for situations where you want to have consistent keys between restarts.
* A Developer can export a key, save it in a vault, and rehydrate it for use in a later run.
* @param jwk
* @returns
*/
static async import(jwk: PrivateKeyJwk): Promise<EcdsaKeypair> {
return EcdsaKeypair.importFromJwk(jwk, { exportable: true })
}
}

Expand Down
66 changes: 50 additions & 16 deletions packages/default-plugins/src/rsa/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ import { webcrypto } from "one-webcrypto"
import * as uint8arrays from "uint8arrays"
import { RSA_DID_PREFIX, RSA_DID_PREFIX_OLD } from "../prefixes.js"
import { didFromKeyBytes, keyBytesFromDid } from "../util.js"
import { AvailableCryptoKeyPair, PrivateKeyJwk } from "../types.js"

export const RSA_ALG = "RSASSA-PKCS1-v1_5"
export const DEFAULT_KEY_SIZE = 2048
export const DEFAULT_HASH_ALG = "SHA-256"
export const SALT_LEGNTH = 128


export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE): Promise<CryptoKeyPair> => {
export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE, exportable: boolean = false): Promise<CryptoKeyPair> => {
return await webcrypto.subtle.generateKey(
{
name: RSA_ALG,
modulusLength: size,
publicExponent: new Uint8Array([ 0x01, 0x00, 0x01 ]),
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: DEFAULT_HASH_ALG }
},
false,
[ "sign", "verify" ]
exportable,
["sign", "verify"]
)
}

Expand All @@ -27,14 +28,47 @@ export const exportKey = async (key: CryptoKey): Promise<Uint8Array> => {
return new Uint8Array(buf)
}

export const exportPrivateKeyJwk = async (keyPair: AvailableCryptoKeyPair): Promise<JsonWebKey> => {
return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey)
}

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

export const importKeypairJwk = async (
privKeyJwk: PrivateKeyJwk,
exportable = false
): Promise<AvailableCryptoKeyPair> => {
const privateKey = await webcrypto.subtle.importKey(
"jwk",
privKeyJwk,
{
name: RSA_ALG,
hash: { name: DEFAULT_HASH_ALG },
},
exportable,
["sign"]
)
const { kty, n, e } = privKeyJwk
const pubKeyJwk = { kty, n, e }
const publicKey = await webcrypto.subtle.importKey(
"jwk",
pubKeyJwk,
{
name: RSA_ALG,
hash: { name: DEFAULT_HASH_ALG },
},
true,
["verify"]
)
return { privateKey, publicKey }
}

export const sign = async (msg: Uint8Array, privateKey: CryptoKey): Promise<Uint8Array> => {
Expand Down Expand Up @@ -100,16 +134,16 @@ export const publicKeyToOldDid = (pubkey: Uint8Array): string => {
*
* 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 ])
const ASN_SEQUENCE_TAG = new Uint8Array([ 0x30 ])
const ASN_BITSTRING_TAG = new Uint8Array([ 0x03 ])
const SPKI_PARAMS_ENCODED = new Uint8Array([48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0])
const ASN_SEQUENCE_TAG = new Uint8Array([0x30])
const ASN_BITSTRING_TAG = new Uint8Array([0x03])

export const convertRSAPublicKeyToSubjectPublicKeyInfo = (rsaPublicKey: Uint8Array): Uint8Array => {
// More info on bitstring encoding: https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-bit-string
const bitStringEncoded = uint8arrays.concat([
ASN_BITSTRING_TAG,
asn1DERLengthEncode(rsaPublicKey.length + 1),
new Uint8Array([ 0x00 ]), // amount of unused bits at the end of our bitstring (counts into length?!)
new Uint8Array([0x00]), // amount of unused bits at the end of our bitstring (counts into length?!)
rsaPublicKey
])
return uint8arrays.concat([
Expand All @@ -129,7 +163,7 @@ export const convertSubjectPublicKeyInfoToRSAPublicKey = (subjectPublicKeyInfo:
// we expect the bitstring next
const bitstringParams = asn1Into(subjectPublicKeyInfo, ASN_BITSTRING_TAG, position)
const bitstring = subjectPublicKeyInfo.subarray(bitstringParams.position, bitstringParams.position + bitstringParams.length)
const unusedBitPadding = bitstring[ 0 ]
const unusedBitPadding = bitstring[0]
if (unusedBitPadding !== 0) {
throw new Error(`Can't convert SPKI to PKCS: Expected bitstring length to be multiple of 8, but got ${unusedBitPadding} unused bits in last byte.`)
}
Expand All @@ -145,7 +179,7 @@ export function asn1DERLengthEncode(length: number): Uint8Array {
}

if (length <= 127) {
return new Uint8Array([ length ])
return new Uint8Array([length])
}

const octets: number[] = []
Expand All @@ -154,23 +188,23 @@ export function asn1DERLengthEncode(length: number): Uint8Array {
length = length >>> 8
}
octets.reverse()
return new Uint8Array([ 0x80 | (octets.length & 0xFF), ...octets ])
return new Uint8Array([0x80 | (octets.length & 0xFF), ...octets])
}

function asn1DERLengthDecodeWithConsumed(bytes: Uint8Array): { number: number; consumed: number } {
if ((bytes[ 0 ] & 0x80) === 0) {
return { number: bytes[ 0 ], consumed: 1 }
if ((bytes[0] & 0x80) === 0) {
return { number: bytes[0], consumed: 1 }
}

const numberBytes = bytes[ 0 ] & 0x7F
const numberBytes = bytes[0] & 0x7F
if (bytes.length < numberBytes + 1) {
throw new Error(`ASN parsing error: Too few bytes. Expected encoded length's length to be at least ${numberBytes}`)
}

let length = 0
for (let i = 0; i < numberBytes; i++) {
length = length << 8
length = length | bytes[ i + 1 ]
length = length | bytes[i + 1]
}
return { number: length, consumed: numberBytes + 1 }
}
Expand Down
Loading
Loading