Skip to content

Commit

Permalink
feat: add support for Multikey (#305)
Browse files Browse the repository at this point in the history
fixes #304

BREAKING CHANGE: The return types have changed for  of `extractPublicKeyBytes()` and `multibaseToBytes()` from Uint8Arrays to Objects containing the `keyBytes: Uint8Array` and a decoded or inferred `keyType: string | undefined`
  • Loading branch information
mirceanis authored Jan 18, 2024
1 parent 764d147 commit 20bbc3e
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 136 deletions.
2 changes: 1 addition & 1 deletion src/ConditionalAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async function verifyConditionDelegated(
}
} else {
try {
foundSigner = await verifyJWTDecoded({ header, payload, data, signature }, delegatedAuthenticator)
foundSigner = verifyJWTDecoded({ header, payload, data, signature }, delegatedAuthenticator)
} catch (e) {
if (!(e as Error).message.startsWith('invalid_signature:')) throw e
}
Expand Down
79 changes: 7 additions & 72 deletions src/JWT.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import canonicalizeData from 'canonicalize'
import { DIDDocument, DIDResolutionResult, parse, ParsedDID, Resolvable, VerificationMethod } from 'did-resolver'
import SignerAlg from './SignerAlgorithm.js'
import { decodeBase64url, EcdsaSignature, encodeBase64url } from './util.js'
import { decodeBase64url, EcdsaSignature, encodeBase64url, KNOWN_JWA, SUPPORTED_PUBLIC_KEY_TYPES } from './util.js'
import VerifierAlgorithm from './VerifierAlgorithm.js'
import { JWT_ERROR } from './Errors.js'
import { verifyProof } from './ConditionalAlgorithm.js'
Expand Down Expand Up @@ -141,79 +141,13 @@ export interface JWTVerified {
policies?: JWTVerifyPolicies
}

export interface PublicKeyTypes {
[name: string]: string[]
}

export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
ES256: ['JsonWebKey2020'],
ES256K: [
'EcdsaSecp256k1VerificationKey2019',
/**
* Equivalent to EcdsaSecp256k1VerificationKey2019 when key is an ethereumAddress
*/
'EcdsaSecp256k1RecoveryMethod2020',
/**
* @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
* not an ethereumAddress
*/
'Secp256k1VerificationKey2018',
/**
* @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
* not an ethereumAddress
*/
'Secp256k1SignatureVerificationKey2018',
/**
* @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
* not an ethereumAddress
*/
'EcdsaPublicKeySecp256k1',
/**
* TODO - support R1 key as well
* 'ConditionalProof2022',
*/
'JsonWebKey2020',
],
'ES256K-R': [
'EcdsaSecp256k1VerificationKey2019',
/**
* Equivalent to EcdsaSecp256k1VerificationKey2019 when key is an ethereumAddress
*/
'EcdsaSecp256k1RecoveryMethod2020',
/**
* @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
* not an ethereumAddress
*/
'Secp256k1VerificationKey2018',
/**
* @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
* not an ethereumAddress
*/
'Secp256k1SignatureVerificationKey2018',
/**
* @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
* not an ethereumAddress
*/
'EcdsaPublicKeySecp256k1',
'ConditionalProof2022',
'JsonWebKey2020',
],
Ed25519: [
'ED25519SignatureVerification',
'Ed25519VerificationKey2018',
'Ed25519VerificationKey2020',
'JsonWebKey2020',
],
EdDSA: ['ED25519SignatureVerification', 'Ed25519VerificationKey2018', 'Ed25519VerificationKey2020', 'JsonWebKey2020'],
}

export const SELF_ISSUED_V2 = 'https://self-issued.me/v2'
export const SELF_ISSUED_V2_VC_INTEROP = 'https://self-issued.me/v2/openid-vc' // https://identity.foundation/jwt-vc-presentation-profile/#id-token-validation
export const SELF_ISSUED_V0_1 = 'https://self-issued.me'

type LegacyVerificationMethod = { publicKey?: string }

const defaultAlg = 'ES256K'
const defaultAlg: KNOWN_JWA = 'ES256K'
const DID_JSON = 'application/did+json'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -347,8 +281,8 @@ export async function createJWT(
}

/**
* Creates a multi-signature signed JWT given multiple issuers and their corresponding signers, and a payload for which the signature is
* over.
* Creates a multi-signature signed JWT given multiple issuers and their corresponding signers, and a payload for
* which the signature is over.
*
* @example
* const signer = ES256KSigner(process.env.PRIVATE_KEY)
Expand Down Expand Up @@ -388,7 +322,8 @@ export async function createMultisignatureJWT(

// Create nested JWT
// See Point 5 of https://www.rfc-editor.org/rfc/rfc7519#section-7.1
// After the first JWT is created (the first JWS), the next JWT is created by inputting the previous JWT as the payload
// After the first JWT is created (the first JWS), the next JWT is created by inputting the previous JWT as the
// payload
if (i !== 0) {
header.cty = 'JWT'
}
Expand Down Expand Up @@ -646,7 +581,7 @@ export async function resolveAuthenticator(
issuer: string,
proofPurpose?: ProofPurposeTypes
): Promise<DIDAuthenticator> {
const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg]
const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg as KNOWN_JWA]
if (!types || types.length === 0) {
throw new Error(`${JWT_ERROR.NOT_SUPPORTED}: No supported signature types for algorithm ${alg}`)
}
Expand Down
60 changes: 16 additions & 44 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { sha256, toEthereumAddress } from './Digest.js'
import type { VerificationMethod } from 'did-resolver'
import {
base58ToBytes,
base64ToBytes,
bytesToBigInt,
bytesToHex,
EcdsaSignature,
ECDSASignature,
hexToBytes,
multibaseToBytes,
extractPublicKeyBytes,
KNOWN_JWA,
stringToBytes,
} from './util.js'
import { verifyBlockchainAccountId } from './blockchains/index.js'
Expand Down Expand Up @@ -42,45 +40,15 @@ export function toSignatureObject2(signature: string, recoverable = false): ECDS
}
}

export function extractPublicKeyBytes(pk: VerificationMethod): Uint8Array {
if (pk.publicKeyBase58) {
return base58ToBytes(pk.publicKeyBase58)
} else if (pk.publicKeyBase64) {
return base64ToBytes(pk.publicKeyBase64)
} else if (pk.publicKeyHex) {
return hexToBytes(pk.publicKeyHex)
} else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'secp256k1' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) {
return secp256k1.ProjectivePoint.fromAffine({
x: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.x)),
y: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.y)),
}).toRawBytes(false)
} else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'P-256' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) {
return p256.ProjectivePoint.fromAffine({
x: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.x)),
y: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.y)),
}).toRawBytes(false)
} else if (
pk.publicKeyJwk &&
pk.publicKeyJwk.kty === 'OKP' &&
['Ed25519', 'X25519'].includes(pk.publicKeyJwk.crv ?? '') &&
pk.publicKeyJwk.x
) {
return base64ToBytes(pk.publicKeyJwk.x)
} else if (pk.publicKeyMultibase) {
return multibaseToBytes(pk.publicKeyMultibase)
}
return new Uint8Array()
}

export function verifyES256(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod {
const hash = sha256(data)
const sig = p256.Signature.fromCompact(toSignatureObject2(signature).compact)
const fullPublicKeys = authenticators.filter((a: VerificationMethod) => !a.ethereumAddress && !a.blockchainAccountId)

const signer: VerificationMethod | undefined = fullPublicKeys.find((pk: VerificationMethod) => {
try {
const pubBytes = extractPublicKeyBytes(pk)
return p256.verify(sig, hash, pubBytes)
const { keyBytes } = extractPublicKeyBytes(pk)
return p256.verify(sig, hash, keyBytes)
} catch (err) {
return false
}
Expand All @@ -106,8 +74,8 @@ export function verifyES256K(

let signer: VerificationMethod | undefined = fullPublicKeys.find((pk: VerificationMethod) => {
try {
const pubBytes = extractPublicKeyBytes(pk)
return secp256k1.verify(signatureNormalized, hash, pubBytes)
const { keyBytes } = extractPublicKeyBytes(pk)
return secp256k1.verify(signatureNormalized, hash, keyBytes)
} catch (err) {
return false
}
Expand Down Expand Up @@ -144,7 +112,8 @@ export function verifyRecoverableES256K(
const recoveredCompressedPublicKeyHex = recoveredPublicKey.toHex(true)

return authenticators.find((a: VerificationMethod) => {
const keyHex = bytesToHex(extractPublicKeyBytes(a))
const { keyBytes } = extractPublicKeyBytes(a)
const keyHex = bytesToHex(keyBytes)
return (
keyHex === recoveredPublicKeyHex ||
keyHex === recoveredCompressedPublicKeyHex ||
Expand Down Expand Up @@ -172,17 +141,20 @@ export function verifyEd25519(
const clear = stringToBytes(data)
const signatureBytes = base64ToBytes(signature)
const signer = authenticators.find((a: VerificationMethod) => {
return ed25519.verify(signatureBytes, clear, extractPublicKeyBytes(a))
const { keyBytes, keyType } = extractPublicKeyBytes(a)
if (keyType === 'Ed25519') {
return ed25519.verify(signatureBytes, clear, keyBytes)
} else {
return false
}
})
if (!signer) throw new Error('invalid_signature: Signature invalid for JWT')
return signer
}

type Verifier = (data: string, signature: string, authenticators: VerificationMethod[]) => VerificationMethod

interface Algorithms {
[name: string]: Verifier
}
type Algorithms = Record<KNOWN_JWA, Verifier>

const algorithms: Algorithms = {
ES256: verifyES256,
Expand All @@ -197,7 +169,7 @@ const algorithms: Algorithms = {
}

function VerifierAlgorithm(alg: string): Verifier {
const impl: Verifier = algorithms[alg]
const impl: Verifier = algorithms[alg as KNOWN_JWA]
if (!impl) throw new Error(`not_supported: Unsupported algorithm ${alg}`)
return impl
}
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/JWT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ describe('verifyJWT() for ES256', () => {
verificationMethod: [
{
id: `${did}#keys-1`,
type: 'JsonWebKey2020',
type: 'EcdsaSecp256r1VerificationKey2019',
controller: did,
publicKeyHex: publicKey,
},
Expand Down
39 changes: 37 additions & 2 deletions src/__tests__/VerifierAlgorithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ describe('ES256', () => {
y: bytesToBase64url(hexToBytes(kp.y.toString(16))),
}
const signer = ES256Signer(privateKey)
const publicKeyMultibase = bytesToMultibase(hexToBytes(publicKey), 'base58btc')
const publicKeyMultibase = bytesToMultibase(hexToBytes(compressedPublicKey), 'base58btc', 'p256-pub')
const publicKeyMultibaseNoCodec = bytesToMultibase(hexToBytes(compressedPublicKey), 'base58btc')

const ecKey1 = {
id: `${did}#keys-1`,
Expand All @@ -79,6 +80,20 @@ describe('ES256', () => {
publicKeyHex: publicKey,
}

const ecKey3 = {
id: `${did}#keys-3`,
type: 'Multikey',
controller: did,
publicKeyMultibase,
}

const ecKey4 = {
id: `${did}#keys-4`,
type: 'Multikey',
controller: did,
publicKeyMultibase: publicKeyMultibaseNoCodec,
}

const compressedKey = {
id: `${did}#keys-4`,
type: 'JsonWebKey2020',
Expand Down Expand Up @@ -155,7 +170,27 @@ describe('ES256', () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }, { alg: 'ES256' })
const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
const pubkey = Object.assign({ publicKeyMultibase }, ecKey2)
const pubkey = ecKey3
// @ts-ignore
return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey)
})

it('validates with publicKeyMultibase Multikey', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }, { alg: 'ES256' })
const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
const pubkey = ecKey3
// @ts-ignore
delete pubkey.publicKeyHex
// @ts-ignore
return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey)
})

it('validates with publicKeyMultibase Multikey without codec', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }, { alg: 'ES256' })
const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
const pubkey = ecKey4
// @ts-ignore
delete pubkey.publicKeyHex
// @ts-ignore
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/xc20pEncryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ describe('xc20pEncryption', () => {
'resolver_error: Could not resolve did:test:3'
)
await expect(resolveX25519Encrypters([did4], resolver)).rejects.toThrowError(
'no_suitable_keys: Could not find x25519 key for did:test:4'
'no_suitable_keys: Could not find X25519 key for did:test:4'
)
await expect(resolveX25519Encrypters([did7], resolver)).rejects.toThrowError(
'no_suitable_keys: Could not find x25519 key for did:test:7'
'no_suitable_keys: Could not find X25519 key for did:test:7'
)
})

Expand Down
23 changes: 16 additions & 7 deletions src/encryption/xc20pEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import type {
Recipient,
WrappingResult,
} from './types.js'
import { base64ToBytes, toSealed } from '../util.js'
import { base64ToBytes, extractPublicKeyBytes, isDefined, toSealed } from '../util.js'
import { xc20pDirDecrypter, xc20pDirEncrypter, xc20pEncrypter } from './xc20pDir.js'
import { computeX25519Ecdh1PUv3Kek, createX25519Ecdh1PUv3Kek } from './X25519-ECDH-1PU.js'
import { computeX25519EcdhEsKek, createX25519EcdhEsKek } from './X25519-ECDH-ES.js'
import { extractPublicKeyBytes } from '../VerifierAlgorithm.js'
import { createFullEncrypter } from './createEncrypter.js'

/**
Expand Down Expand Up @@ -175,12 +174,22 @@ export async function resolveX25519Encrypters(dids: string[], resolver: Resolvab
})
?.filter((key) => typeof key !== 'undefined') as VerificationMethod[]
const pks =
agreementKeys?.filter((key) => {
return key.type === 'X25519KeyAgreementKey2019' || key.type === 'X25519KeyAgreementKey2020'
}) || []
agreementKeys?.filter((key) =>
['X25519KeyAgreementKey2019', 'X25519KeyAgreementKey2020', 'JsonWebKey2020', 'Multikey'].includes(key.type)
) ?? []
if (!pks.length && !controllerEncrypters.length)
throw new Error(`no_suitable_keys: Could not find x25519 key for ${did}`)
return pks.map((pk) => x25519Encrypter(extractPublicKeyBytes(pk), pk.id)).concat(...controllerEncrypters)
throw new Error(`no_suitable_keys: Could not find X25519 key for ${did}`)
return pks
.map((pk) => {
const { keyBytes, keyType } = extractPublicKeyBytes(pk)
if (keyType === 'X25519') {
return x25519Encrypter(keyBytes, pk.id)
} else {
return null
}
})
.filter(isDefined)
.concat(...controllerEncrypters)
}

const encrypterPromises = dids.map((did) => encryptersForDID(did))
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ export {
multibaseToBytes,
bytesToMultibase,
supportedCodecs,
extractPublicKeyBytes,
} from './util.js'

export { extractPublicKeyBytes } from './VerifierAlgorithm.js'

export * from './Errors.js'
Loading

0 comments on commit 20bbc3e

Please sign in to comment.