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

Developing a Signer for P-256. (work in progress) #212

Closed
wants to merge 50 commits into from
Closed
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
01cad8d
add ES256Signer Module
bshambaugh Jan 10, 2022
3665f66
update base58 test, it appears to have a problem
bshambaugh Jan 10, 2022
b63394e
update with failing test for base58
bshambaugh Jan 10, 2022
fc6dbbf
modify base58matcher to match both base58 in es256 and es256k
bshambaugh Jan 16, 2022
11583f6
change 68 to 86 in base58 matcher
bshambaugh Jan 16, 2022
28dd20b
remove uneccessary = sign in the base64 regex
bshambaugh Jan 16, 2022
a5c3cd9
delete npm.txt which is a temporary test output
bshambaugh Jan 16, 2022
bdc2bdb
add ES256Signer to index so it is acessible
bshambaugh Jan 17, 2022
d8a66f9
refactor SignerAlgorithm.ts and SignerAlgorithm.test.ts by breaking u…
bshambaugh Jan 18, 2022
31ab707
move tests for signers into folder called signers
bshambaugh Jan 18, 2022
4ccb1dd
remove carriage returns (should be result of enter) and unused variab…
bshambaugh Jan 18, 2022
4397a49
break up verifierAlgorithm into separate files bases on Curve
bshambaugh Jan 18, 2022
5ef3a89
add preliminary tests for JWT
bshambaugh Jan 19, 2022
fdbbfd7
refactor VerifierAlgorithm test
bshambaugh Jan 20, 2022
75dc01b
update JWT tests
bshambaugh Jan 22, 2022
369c419
update ES256Signer with constants
bshambaugh Jan 22, 2022
f936324
change the name of constants to common in JWT test folder
bshambaugh Jan 23, 2022
c06dc4b
add tests for JWT common
bshambaugh Jan 24, 2022
eda4633
add ES256 to Signer Verifier and JWT ...tests needed
bshambaugh Jan 24, 2022
d4c194b
add note about supported key types in JWT, Signer, and Verifier Algor…
bshambaugh Jan 25, 2022
45d2df8
add p256 to /src/VerifierAlg/common.ts
bshambaugh Jan 25, 2022
4cc5bff
rename common files for Signer, Verifier, and JWT test
bshambaugh Jan 27, 2022
cc6e4ec
in common_Signer_test_test.ts the create JWK functions need to be tes…
bshambaugh Feb 7, 2022
933a4ff
add more tests for JWT/JWT.ES256Signer.test.ts , pad so common_Signer…
bshambaugh Mar 1, 2022
920ad85
commit latest changes .. fix tests in ES256SignerAlg.test.ts
bshambaugh Mar 3, 2022
831e0f0
add EcdsaSignature interface to src/SignerAlg/common_SignerAlg.ts
bshambaugh Mar 4, 2022
09b50a9
change to standard JWK crv parameter from secp256r1 to P-256 in Verif…
bshambaugh Mar 4, 2022
5cf650b
add recoverySigner and signature result for test in ES256Signer in Si…
bshambaugh Mar 4, 2022
8a38bb4
added EcdsaSecp256r1VerificationKey2022 as valid type for publicKeyBa…
bshambaugh Mar 4, 2022
e3fa780
commit some changes to satisfy linter
bshambaugh Mar 5, 2022
ace72fa
add lint recommendation and rename common_Signer_test to CommonSigner…
bshambaugh Mar 6, 2022
a4afd07
emulate fix: use uint8arrays instead of Buffer for new files
bshambaugh Mar 6, 2022
07c0be9
renamed common_VerifierAlg.ts to CommonVerifierAlg.ts and changed ref…
bshambaugh Mar 6, 2022
4aaf7a5
move jwk-to-pem from dependency to devDependency in package.json
bshambaugh Mar 6, 2022
4fc2dbd
uncommented LegacyVerificationMethod semantic type to fix build
bshambaugh Mar 6, 2022
3190cf9
remove yarn from package.json
bshambaugh Mar 7, 2022
dc04d0c
removed comma from package.json, so it parses
bshambaugh Mar 10, 2022
03e8e52
added P256 for cosmos, but might have to yank b/c eip155 for P-256 no…
bshambaugh Mar 10, 2022
8294a6b
Merge branch 'cosmos' , bring in support for P-256 if warranted
bshambaugh Mar 10, 2022
3a65a2a
added test to for comosmos adress from public key to keep consistent …
bshambaugh Mar 10, 2022
252688c
add test for conssistency for publicKey to cosmos address
bshambaugh Mar 10, 2022
164537b
commit stub for decrypt / encrypt JWE Decrypter impl. for P-256
bshambaugh Mar 10, 2022
7c881f1
removed package-lock.json at mirceanis request
bshambaugh Mar 11, 2022
260e214
change imports for Verifier and Signer Algorithms
bshambaugh Mar 22, 2022
f9d8aba
remove LegacyVerificationMethod interface and only keep in CommonVeri…
bshambaugh Mar 22, 2022
aef3643
reverse base64matcher to original version that will match '=' sign, o…
bshambaugh Mar 22, 2022
764f89f
add a second matcher for base58 to util.test.ts to illustrate variabl…
bshambaugh Mar 27, 2022
05e883f
VerificationMethodCommonSigner is available as VerificationMethod in …
bshambaugh Mar 28, 2022
b768725
rewrite DIDDocumentCommonSigner to use existing DIDDocument interface…
bshambaugh Mar 28, 2022
0b2f18c
bring in JsonWebKey Interface from did-resolver instead of declaring …
bshambaugh Mar 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30,769 changes: 30,769 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@semantic-release/git": "9.0.1",
"@types/elliptic": "6.4.14",
"@types/jest": "27.0.3",
"@types/jsonwebtoken": "^8.5.8",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"codecov": "3.8.2",
Expand All @@ -67,6 +68,7 @@
"eslint-plugin-prettier": "4.0.0",
"jest": "27.4.5",
"jsontokens": "3.0.0",
"jwk-to-pem": "^2.0.5",
"microbundle": "0.14.2",
"mockdate": "3.0.5",
"prettier": "2.5.1",
Expand Down
3 changes: 2 additions & 1 deletion src/JWE.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fromString } from 'uint8arrays'
import { base64ToBytes, bytesToBase64url, decodeBase64url, toSealed } from './util'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -128,7 +129,7 @@ export async function decryptJWE(jwe: JWE, decrypter: Decrypter): Promise<Uint8A
if (protHeader.enc !== decrypter.enc)
throw new Error(`not_supported: Decrypter does not supported: '${protHeader.enc}'`)
const sealed = toSealed(jwe.ciphertext, jwe.tag)
const aad = new Uint8Array(Buffer.from(jwe.aad ? `${jwe.protected}.${jwe.aad}` : jwe.protected))
const aad = fromString(jwe.aad ? `${jwe.protected}.${jwe.aad}` : jwe.protected)
let cleartext = null
if (protHeader.alg === 'dir' && decrypter.alg === 'dir') {
cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad)
Expand Down
2 changes: 2 additions & 0 deletions src/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface PublicKeyTypes {
[name: string]: string[]
}
export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
ES256: ['JsonWebKey2020', 'EcdsaSecp256r1VerificationKey2022'],
'ES256-R': ['JsonWebKey2020', 'EcdsaSecp256r1VerificationKey2022'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ES256-R really needed? Afaik no one uses this?

Copy link
Contributor Author

@bshambaugh bshambaugh Mar 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It clearly seemed like a non-standard key type. Adding a recoverySigner was a "oh why not" kind of thing. However, 'ES256-K' was a use your own discretion kind of thing (reference: #146) and I suppose 'ES256-R' would be as well if included. I'd need to know the logic behind having it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @oed, ES256-R is not needed.
ES256K-R existed as a pattern and was in use before it got a name.
It exists now to allow for blockchainAccountId verification methods to be used as verifiers.
We could probably phase it out of existence if we accept 1 bit less of security for ES256K + blockchainAccountId

Copy link
Contributor Author

@bshambaugh bshambaugh Mar 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I have the code for ES256-R. I can archive it for now, and reconsider it later. (?) The reason for this PR was to support something like https://github.com/bshambaugh/key-did-provider-p256/blob/master/src/index.ts . This is a non-functional sketch (at the moment) to support EIP-2844 (https://eips.ethereum.org/EIPS/eip-2844) . At the EthDenver Hackathon I heard about https://f-o-a-m.github.io/foam.developer/foamlite/end-node.html which mentions use of a recovery method that grabs the public key based on a signature. (I discovered that they are also using LoRa.) I literally just read this 5 or 10 minutes ago so I am not educated in the use nor in foam. I think that their protocol is even lower level than what I am attempting.

https://www.youtube.com/watch?v=Z8Wf7Srsg5U&t=50m20s is a Ceramic Community Call that mentions Joel's (oed's) introduction of EIP-2844 to my problem which sent me down this road. Since this (described in the Call) is a preexisting project I may not apply it (or may not be allowed to) to the EthDenver hackathon. In the next week or so, as the hackathon event rolls up, I may in fact try something else, which could distract me from this PR started in January. For context, this is the project I was mentioning in the Community Call: https://github.com/bshambaugh/BlinkyProject .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @oed, ES256-R is not needed. ES256K-R existed as a pattern and was in use before it got a name. It exists now to allow for blockchainAccountId verification methods to be used as verifiers. We could probably phase it out of existence if we accept 1 bit less of security for ES256K + blockchainAccountId

I'll have to peruse the code to understand the context. Since Joel knows about what I have been up to, I assume I can just prune it out for now.

ES256K: [
'EcdsaSecp256k1VerificationKey2019',
/**
Expand Down
6 changes: 6 additions & 0 deletions src/SignerAlg/CommonSignerAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { EcdsaSignature } from '../util'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function instanceOfEcdsaSignature(object: any): object is EcdsaSignature {
return typeof object === 'object' && 'r' in object && 's' in object
}
17 changes: 17 additions & 0 deletions src/SignerAlg/ES256KSignerAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Signer, SignerAlgorithm } from '../JWT'
import { EcdsaSignature, fromJose, toJose } from '../util'
import * as CommonSignerAlg from './CommonSignerAlg'

export function ES256KSignerAlg(recoverable?: boolean): SignerAlgorithm {
return async function sign(payload: string, signer: Signer): Promise<string> {
const signature: EcdsaSignature | string = await signer(payload)
if (CommonSignerAlg.instanceOfEcdsaSignature(signature)) {
return toJose(signature, recoverable)
} else {
if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') {
throw new Error(`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`)
}
return signature
}
}
}
17 changes: 17 additions & 0 deletions src/SignerAlg/ES256SignerAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Signer, SignerAlgorithm } from '../JWT'
import { EcdsaSignature, fromJose, toJose } from '../util'
import * as CommonSignerAlg from './CommonSignerAlg'

export function ES256SignerAlg(recoverable?: boolean): SignerAlgorithm {
return async function sign(payload: string, signer: Signer): Promise<string> {
const signature: EcdsaSignature | string = await signer(payload)
if (CommonSignerAlg.instanceOfEcdsaSignature(signature)) {
return toJose(signature, recoverable)
} else {
if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') {
throw new Error(`not_supported: ES256-R not supported when signer doesn't provide a recovery param`)
}
return signature
}
}
}
14 changes: 14 additions & 0 deletions src/SignerAlg/Ed25519SignerAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Signer, SignerAlgorithm } from '../JWT'
import { EcdsaSignature } from '../util'
import * as CommonSignerAlg from './CommonSignerAlg'

export function Ed25519SignerAlg(): SignerAlgorithm {
return async function sign(payload: string, signer: Signer): Promise<string> {
const signature: EcdsaSignature | string = await signer(payload)
if (!CommonSignerAlg.instanceOfEcdsaSignature(signature)) {
return signature
} else {
throw new Error('invalid_config: expected a signer function that returns a string instead of signature object')
}
}
}
44 changes: 10 additions & 34 deletions src/SignerAlgorithm.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,25 @@
import { Signer, SignerAlgorithm } from './JWT'
import { EcdsaSignature, fromJose, toJose } from './util'
import { SignerAlgorithm } from './JWT'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function instanceOfEcdsaSignature(object: any): object is EcdsaSignature {
return typeof object === 'object' && 'r' in object && 's' in object
}

export function ES256KSignerAlg(recoverable?: boolean): SignerAlgorithm {
return async function sign(payload: string, signer: Signer): Promise<string> {
const signature: EcdsaSignature | string = await signer(payload)
if (instanceOfEcdsaSignature(signature)) {
return toJose(signature, recoverable)
} else {
if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') {
throw new Error(`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`)
}
return signature
}
}
}
import * as ES256KSignerAlg from './SignerAlg/ES256KSignerAlg'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports can be rephrased as:

Suggested change
import * as ES256KSignerAlg from './SignerAlg/ES256KSignerAlg'
import { ES256KSignerAlg } from './SignerAlg/ES256KSignerAlg'

and then the code below is simplified as well

Copy link
Contributor Author

@bshambaugh bshambaugh Mar 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understandable. It might have a bit to do with how my mind was working. I grasped things as, and after, I worked through them. I realize it was a huge refactor. It seemed necessary as otherwise the files were getting to get huge combining multiple signers and it would be hard to follow. I told Joel (@ oed) that I was trying to figure things out for my particular project. I need a remote signer over a radio connection. I see mention of constructing a remote signer. I went ahead and added ES256 and walked through the library so I'd have it if I need it when bringing in https://github.com/decentralized-identity/did-jwt-vc later. I'm also trying to build something like this (https://github.com/ceramicnetwork/key-did-provider-ed25519) but for a remote signer and with the P-256 curve (because I'm using a cryptographic co-processor that only provides this curve https://github.com/sparkfun/SparkFun_ATECCX08a_Arduino_Library). Yeah, I was kind of aggressive with my commit. I think I have a got to get this done mentality. I've been working on a particular (presently unfunded/hobby/or whatever project) for over a year, and get onto myself for not making enough progress to be successful with it. To my knowledge, adding NIST curves is not a big ceramic/3box initiative (they're focusing on scaling I think now). I ask questions from time to time, and I see this effort as the other half to js-ceramic/key-did-resolver. (https://github.com/ceramicnetwork/js-ceramic/tree/develop/packages/key-did-resolver/src) . I think this eventually got into the repo because I was relentless and they wanted to support active projects going for their ceramic mainnet initiative. I'll be patient. Maybe in time I'll discover that I did some work that did not need to be done. I realize other people have to deal with what I put out. Maybe I'm at a point where I can talk intelligently about it, if I did not realize before... apologies ...

fwiw, right now I'm looking at blockchain/cosmos.ts .... I'm trying to pull in a function that will default to secp256k1 , but will also allow for (P-256) secp256r1 because the publicKey needs to be compressed before construction in the rest of the function.

fwiw, the test files were broken up into smaller files, and parts of them were cloned (swapping the secp256r1 curve for the secp256k1 curve)

Again understandable... (+41,203 −9,451 ) is a huge change for a well used library. It kind of seemed like having fun on some trip, but then later getting the credit card bill. Since I am presently in busy beaver mode I'm sure I'll find some other things to do so no worries if you've got some other things to do as well (which I expect).

Edit: for the sake of transparency I reached out via e-mail to @kimdhamilton because I felt new to typescript and good code quality and this library and verifiable credentials specialization. We seem to get along well, but I'm not sure I should have done that privately. I think she uses this library in some of her other work, however I am not sure if she's ever reviewed or built on it before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to apologise :)

The bill of changes will be reduced once you remove package-lock.json since this project uses yarn instead, and further more after removing ES256-R and related code.

It might not be worth trying to adapt the blockchain/cosmos bits since AFAIK there is no use of P-256 on the blockchains involved, only secp256k.

Still, there is very much to review, and it will take very long.

import * as Ed25519SignerAlg from './SignerAlg/Ed25519SignerAlg'

export function Ed25519SignerAlg(): SignerAlgorithm {
return async function sign(payload: string, signer: Signer): Promise<string> {
const signature: EcdsaSignature | string = await signer(payload)
if (!instanceOfEcdsaSignature(signature)) {
return signature
} else {
throw new Error('invalid_config: expected a signer function that returns a string instead of signature object')
}
}
}
import * as ES256SignerAlg from './SignerAlg/ES256SignerAlg'

interface SignerAlgorithms {
[alg: string]: SignerAlgorithm
}

const algorithms: SignerAlgorithms = {
ES256K: ES256KSignerAlg(),
ES256: ES256SignerAlg.ES256SignerAlg(),
'ES256-R': ES256SignerAlg.ES256SignerAlg(true),
ES256K: ES256KSignerAlg.ES256KSignerAlg(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the above suggestion, this would simplify to:

Suggested change
ES256K: ES256KSignerAlg.ES256KSignerAlg(),
ES256K: ES256KSignerAlg(),

// This is a non-standard algorithm but retained for backwards compatibility
// see https://github.com/decentralized-identity/did-jwt/issues/146
'ES256K-R': ES256KSignerAlg(true),
'ES256K-R': ES256KSignerAlg.ES256KSignerAlg(true),
// This is actually incorrect but retained for backwards compatibility
// see https://github.com/decentralized-identity/did-jwt/issues/130
Ed25519: Ed25519SignerAlg(),
EdDSA: Ed25519SignerAlg(),
Ed25519: Ed25519SignerAlg.Ed25519SignerAlg(),
EdDSA: Ed25519SignerAlg.Ed25519SignerAlg(),
}

function SignerAlg(alg: string): SignerAlgorithm {
Expand Down
59 changes: 59 additions & 0 deletions src/VerifierAlg/CommonVerifierAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ec as EC } from 'elliptic'
import type { VerificationMethod } from 'did-resolver'
import { bases } from 'multiformats/basics'
import { hexToBytes, base58ToBytes, base64ToBytes, bytesToHex, EcdsaSignature } from '../util'

const secp256k1 = new EC('secp256k1')
const secp256r1 = new EC('p256')

// converts a JOSE signature to it's components
export function toSignatureObject(signature: string, recoverable = false): EcdsaSignature {
const rawSig: Uint8Array = base64ToBytes(signature)
if (rawSig.length !== (recoverable ? 65 : 64)) {
throw new Error('wrong signature length')
}
const r: string = bytesToHex(rawSig.slice(0, 32))
const s: string = bytesToHex(rawSig.slice(32, 64))
const sigObj: EcdsaSignature = { r, s }
if (recoverable) {
sigObj.recoveryParam = rawSig[64]
}
return sigObj
}

interface LegacyVerificationMethod extends VerificationMethod {
publicKeyBase64: string
}

export function extractPublicKeyBytes(pk: VerificationMethod): Uint8Array {
if (pk.publicKeyBase58) {
return base58ToBytes(pk.publicKeyBase58)
} else if ((<LegacyVerificationMethod>pk).publicKeyBase64) {
return base64ToBytes((<LegacyVerificationMethod>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 hexToBytes(
secp256k1
.keyFromPublic({
x: bytesToHex(base64ToBytes(pk.publicKeyJwk.x)),
y: bytesToHex(base64ToBytes(pk.publicKeyJwk.y)),
})
.getPublic('hex')
)
} else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'P-256' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) {
return hexToBytes(
secp256r1
.keyFromPublic({
x: bytesToHex(base64ToBytes(pk.publicKeyJwk.x)),
y: bytesToHex(base64ToBytes(pk.publicKeyJwk.y)),
})
.getPublic('hex')
)
} 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 new Uint8Array()
}
91 changes: 91 additions & 0 deletions src/VerifierAlg/ES256KVerifierAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ec as EC, SignatureInput } from 'elliptic'
import { sha256, toEthereumAddress } from '../Digest'
import type { VerificationMethod } from 'did-resolver'
import { bytesToHex, EcdsaSignature } from '../util'
import { verifyBlockchainAccountId } from '../blockchains'
import * as CommonVerifierAlg from './CommonVerifierAlg'

const secp256k1 = new EC('secp256k1')

/*
interface LegacyVerificationMethod extends VerificationMethod {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer this simple extension of the publicly defined type as opposed to redefining the interfaces all together.
It seems clearer about what the legacy bits are.

publicKeyBase64: string
}
*/

export function verifyES256K(
data: string,
signature: string,
authenticators: VerificationMethod[]
): VerificationMethod {
const hash: Uint8Array = sha256(data)
const sigObj: EcdsaSignature = CommonVerifierAlg.toSignatureObject(signature)
const fullPublicKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress === 'undefined' && typeof blockchainAccountId === 'undefined'
})
const blockchainAddressKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress !== 'undefined' || typeof blockchainAccountId !== 'undefined'
})

let signer: VerificationMethod | undefined = fullPublicKeys.find((pk: VerificationMethod) => {
try {
const pubBytes = CommonVerifierAlg.extractPublicKeyBytes(pk)
return secp256k1.keyFromPublic(pubBytes).verify(hash, <SignatureInput>sigObj)
} catch (err) {
return false
}
})

if (!signer && blockchainAddressKeys.length > 0) {
signer = verifyRecoverableES256K(data, signature, blockchainAddressKeys)
}

if (!signer) throw new Error('invalid_signature: Signature invalid for JWT')
return signer
}

export function verifyRecoverableES256K(
data: string,
signature: string,
authenticators: VerificationMethod[]
): VerificationMethod {
let signatures: EcdsaSignature[]
if (signature.length > 86) {
signatures = [CommonVerifierAlg.toSignatureObject(signature, true)]
} else {
const so = CommonVerifierAlg.toSignatureObject(signature, false)
signatures = [
{ ...so, recoveryParam: 0 },
{ ...so, recoveryParam: 1 },
]
}

const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): VerificationMethod | undefined => {
const hash: Uint8Array = sha256(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const recoveredKey: any = secp256k1.recoverPubKey(hash, <SignatureInput>sigObj, <number>sigObj.recoveryParam)
const recoveredPublicKeyHex: string = recoveredKey.encode('hex')
const recoveredCompressedPublicKeyHex: string = recoveredKey.encode('hex', true)
const recoveredAddress: string = toEthereumAddress(recoveredPublicKeyHex)

const signer: VerificationMethod | undefined = authenticators.find((pk: VerificationMethod) => {
const keyHex = bytesToHex(CommonVerifierAlg.extractPublicKeyBytes(pk))
return (
keyHex === recoveredPublicKeyHex ||
keyHex === recoveredCompressedPublicKeyHex ||
pk.ethereumAddress?.toLowerCase() === recoveredAddress ||
pk.blockchainAccountId?.split('@eip155')?.[0].toLowerCase() === recoveredAddress || // CAIP-2
verifyBlockchainAccountId(recoveredPublicKeyHex, pk.blockchainAccountId) // CAIP-10
)
})

return signer
}

const signer: VerificationMethod[] = signatures
.map(checkSignatureAgainstSigner)
.filter((key) => typeof key !== 'undefined') as VerificationMethod[]

if (signer.length === 0) throw new Error('invalid_signature: Signature invalid for JWT')
return signer[0]
}
87 changes: 87 additions & 0 deletions src/VerifierAlg/ES256VerifierAlg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ec as EC, SignatureInput } from 'elliptic'
import { sha256, toEthereumAddress } from '../Digest'
import type { VerificationMethod } from 'did-resolver'
import { bytesToHex, EcdsaSignature } from '../util'
import { verifyBlockchainAccountId } from '../blockchains'
import * as CommonVerifierAlg from './CommonVerifierAlg'

const secp256r1 = new EC('p256')

/*
interface LegacyVerificationMethod extends VerificationMethod {
publicKeyBase64: string
}
*/

export function verifyES256(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod {
const hash: Uint8Array = sha256(data)
const sigObj: EcdsaSignature = CommonVerifierAlg.toSignatureObject(signature)
const fullPublicKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress === 'undefined' && typeof blockchainAccountId === 'undefined'
})
const blockchainAddressKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress !== 'undefined' || typeof blockchainAccountId !== 'undefined'
})

let signer: VerificationMethod | undefined = fullPublicKeys.find((pk: VerificationMethod) => {
try {
const pubBytes = CommonVerifierAlg.extractPublicKeyBytes(pk)
return secp256r1.keyFromPublic(pubBytes).verify(hash, <SignatureInput>sigObj)
} catch (err) {
return false
}
})

if (!signer && blockchainAddressKeys.length > 0) {
signer = verifyRecoverableES256(data, signature, blockchainAddressKeys)
}

if (!signer) throw new Error('invalid_signature: Signature invalid for JWT')
return signer
}

export function verifyRecoverableES256(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not needed since ES256-R is not needed

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, ES256-R is not and probably will never be a thing.... ES256K-R is a thing already... even though it remains unregistered... I would discourage adding more "registered stuff" here.

data: string,
signature: string,
authenticators: VerificationMethod[]
): VerificationMethod {
let signatures: EcdsaSignature[]
if (signature.length > 86) {
signatures = [CommonVerifierAlg.toSignatureObject(signature, true)]
} else {
const so = CommonVerifierAlg.toSignatureObject(signature, false)
signatures = [
{ ...so, recoveryParam: 0 },
{ ...so, recoveryParam: 1 },
]
}

const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): VerificationMethod | undefined => {
const hash: Uint8Array = sha256(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const recoveredKey: any = secp256r1.recoverPubKey(hash, <SignatureInput>sigObj, <number>sigObj.recoveryParam)
const recoveredPublicKeyHex: string = recoveredKey.encode('hex')
const recoveredCompressedPublicKeyHex: string = recoveredKey.encode('hex', true)
const recoveredAddress: string = toEthereumAddress(recoveredPublicKeyHex)

const signer: VerificationMethod | undefined = authenticators.find((pk: VerificationMethod) => {
const keyHex = bytesToHex(CommonVerifierAlg.extractPublicKeyBytes(pk))
return (
keyHex === recoveredPublicKeyHex ||
keyHex === recoveredCompressedPublicKeyHex ||
pk.ethereumAddress?.toLowerCase() === recoveredAddress ||
pk.blockchainAccountId?.split('@eip155')?.[0].toLowerCase() === recoveredAddress || // CAIP-2
verifyBlockchainAccountId(recoveredPublicKeyHex, pk.blockchainAccountId) // CAIP-10
)
})

return signer
}

const signer: VerificationMethod[] = signatures
.map(checkSignatureAgainstSigner)
.filter((key) => typeof key !== 'undefined') as VerificationMethod[]

if (signer.length === 0) throw new Error('invalid_signature: Signature invalid for JWT')
return signer[0]
}
Loading