Skip to content

Commit

Permalink
feat: interpret publicKeyMultibase as multicodec (#298)
Browse files Browse the repository at this point in the history
* fix: try to interpret publicKeyMultibase as multicodec

fixes #297

* feat: export byte conversion utils
  • Loading branch information
mirceanis authored Sep 27, 2023
1 parent b358060 commit bf76cea
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 10 deletions.
14 changes: 7 additions & 7 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { sha256, toEthereumAddress } from './Digest.js'
import type { VerificationMethod } from 'did-resolver'
import { bases } from 'multiformats/basics'
import {
hexToBytes,
base58ToBytes,
base64ToBytes,
bytesToBigInt,
bytesToHex,
EcdsaSignature,
stringToBytes,
bytesToBigInt,
ECDSASignature,
hexToBytes,
multibaseToBytes,
stringToBytes,
} from './util.js'
import { verifyBlockchainAccountId } from './blockchains/index.js'
import { secp256k1 } from '@noble/curves/secp256k1'
Expand Down Expand Up @@ -67,9 +67,7 @@ export function extractPublicKeyBytes(pk: VerificationMethod): Uint8Array {
) {
return base64ToBytes(pk.publicKeyJwk.x)
} else if (pk.publicKeyMultibase) {
const { base16, base58btc, base64, base64url } = bases
const baseDecoder = base16.decoder.or(base58btc.decoder.or(base64.decoder.or(base64url.decoder)))
return baseDecoder.decode(pk.publicKeyMultibase)
return multibaseToBytes(pk.publicKeyMultibase)
}
return new Uint8Array()
}
Expand Down Expand Up @@ -181,9 +179,11 @@ export function verifyEd25519(
}

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

interface Algorithms {
[name: string]: Verifier
}

const algorithms: Algorithms = {
ES256: verifyES256,
ES256K: verifyES256K,
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/VerifierAlgorithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ const publicKeyJwk = {
y: bytesToBase64url(bigintToBytes(publicKeyPoint.y, 32)),
}
const publicKeyMultibase = bytesToMultibase(publicKeyBytes, 'base58btc')
const publicKeyMultibaseMulticodec = bytesToMultibase(publicKeyBytes, 'base58btc', 'secp256k1-pub')
const eip155 = toEthereumAddress(publicKeyHex)
const bip122 = toBip122Address(publicKeyHex, 'undefined')
const cosmosPrefix = 'example'
Expand Down Expand Up @@ -384,6 +385,17 @@ describe('ES256K', () => {
return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey)
})

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

it('validates signature with compressed public key and picks correct public key', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer })
Expand Down
51 changes: 51 additions & 0 deletions src/__tests__/didkey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,55 @@ describe('Ed25519', () => {
},
})
})

it('handles EdDSA algorithm with did:peer', async () => {
expect.assertions(1)

const resolver = {
resolve: async () => ({
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: 'application/did+ld+json',
},
didDocument: {
'@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'],
id: 'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
verificationMethod: [
{
id: 'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw#6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
type: 'Ed25519VerificationKey2020',
controller: 'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
publicKeyMultibase: 'z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
},
],
authentication: [
'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw#6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
],
assertionMethod: [
'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw#6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
],
capabilityInvocation: [
'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw#6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
],
capabilityDelegation: [
'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw#6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
],
},
}),
}
const jwt =
'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5vdGhpbmciOiJlbHNlIG1hdHRlcnMifX0sIm5iZiI6MTY5NTA1MjE4MSwiaXNzIjoiZGlkOnBlZXI6MHo2TWtuTlc1bXZyVXBzc1NKd1pSUVNpbkxXWHpjRUNQdGp6ZUtVc1RSMU12dW1mdyJ9.mvgdqscXYjIXRuut83e8AfcBVdQJJOppQ9flohALoke_qRL9rR0FBOuBjWbf6uHftKv8lqUcqZuPnmsAJ0sbAA'
const { payload } = await verifyJWT(jwt, { resolver })
return expect(payload).toMatchObject({
iss: 'did:peer:0z6MknNW5mvrUpssSJwZRQSinLWXzcECPtjzeKUsTR1Mvumfw',
nbf: 1695052181,
vc: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
credentialSubject: {
nothing: 'else matters',
},
type: ['VerifiableCredential'],
},
})
})
})
15 changes: 14 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ export {

export { type JWTOptions, type JWTVerifyOptions } from './JWT.js'

export { base64ToBytes, base58ToBytes, hexToBytes, genX25519EphemeralKeyPair } from './util.js'
export {
base64ToBytes,
bytesToBase64url,
base58ToBytes,
bytesToBase58,
hexToBytes,
bytesToHex,
genX25519EphemeralKeyPair,
multibaseToBytes,
bytesToMultibase,
supportedCodecs,
} from './util.js'

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

export * from './Errors.js'
78 changes: 76 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { concat, fromString, toString } from 'uint8arrays'
import { bases } from 'multiformats/basics'
import { x25519 } from '@noble/curves/ed25519'
import type { EphemeralKeyPair } from './encryption/types.js'
import { varint } from 'multiformats'

const u8a = { toString, fromString, concat }

Expand Down Expand Up @@ -52,8 +53,81 @@ export function bytesToBase58(b: Uint8Array): string {
return u8a.toString(b, 'base58btc')
}

export function bytesToMultibase(b: Uint8Array, base: keyof typeof bases): string {
return bases[base].encode(b)
// this is from the multicodec table https://github.com/multiformats/multicodec/blob/master/table.csv
export const supportedCodecs = {
'ed25519-pub': 0xed,
'x25519-pub': 0xec,
'secp256k1-pub': 0xe7,
'bls12_381-g1-pub': 0xea,
'bls12_381-g2-pub': 0xeb,
'p256-pub': 0x1200,
}

/**
* Encodes the given byte array to a multibase string (defaulting to base58btc).
* If a codec is provided, the corresponding multicodec prefix will be added.
*
* @param b - the Uint8Array to be encoded
* @param base - the base to use for encoding (defaults to base58btc)
* @param codec - the codec to use for encoding (defaults to no codec)
*
* @returns the multibase encoded string
*
* @public
*/
export function bytesToMultibase(
b: Uint8Array,
base: keyof typeof bases = 'base58btc',
codec?: keyof typeof supportedCodecs | number
): string {
if (!codec) {
return bases[base].encode(b)
} else {
const codecCode = typeof codec === 'string' ? supportedCodecs[codec] : codec
const prefixLength = varint.encodingLength(codecCode)
const multicodecEncoding = new Uint8Array(prefixLength + b.length)
varint.encodeTo(codecCode, multicodecEncoding) // set prefix
multicodecEncoding.set(b, prefixLength) // add the original bytes
return bases[base].encode(multicodecEncoding)
}
}

/**
* Converts a multibase string to the Uint8Array it represents.
* This method will assume the byte array that is multibase encoded is a multicodec and will attempt to decode it.
*
* @param s - the string to be converted
*
* @throws if the string is not formatted correctly.
*
* @public
*/
export function multibaseToBytes(s: string): Uint8Array {
const { base10, base16, base16upper, base58btc, base64, base64url } = bases

const baseDecoder = base58btc.decoder
.or(base10.decoder)
.or(base16.decoder)
.or(base16upper.decoder)
.or(base64.decoder)
.or(base64url.decoder)
const bytes = baseDecoder.decode(s)

// look for known key lengths first
// Ed25519/X25519, secp256k1/P256 compressed or not, BLS12-381 G1/G2 compressed
if ([32, 33, 48, 64, 65, 96].includes(bytes.length)) {
return bytes
}

// then assume multicodec, otherwise return the bytes
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [codec, length] = varint.decode(bytes)
return bytes.slice(length)
} catch (e) {
// not a multicodec, return the bytes
return bytes
}
}

export function hexToBytes(s: string, minLength?: number): Uint8Array {
Expand Down

0 comments on commit bf76cea

Please sign in to comment.