-
Notifications
You must be signed in to change notification settings - Fork 72
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
Changes from all commits
01cad8d
3665f66
b63394e
fc6dbbf
11583f6
28dd20b
a5c3cd9
bdc2bdb
d8a66f9
31ab707
4ccb1dd
4397a49
5ef3a89
fdbbfd7
75dc01b
369c419
f936324
c06dc4b
eda4633
d4c194b
45d2df8
4cc5bff
cc6e4ec
933a4ff
920ad85
831e0f0
09b50a9
5cf650b
8a38bb4
e3fa780
ace72fa
a4afd07
07c0be9
4aaf7a5
4fc2dbd
3190cf9
dc04d0c
03e8e52
8294a6b
3a65a2a
252688c
164537b
7c881f1
260e214
f9d8aba
aef3643
764f89f
05e883f
b768725
0b2f18c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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 | ||
} | ||
} | ||
} |
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 | ||
} | ||
} | ||
} |
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') | ||
} | ||
} | ||
} |
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() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
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') | ||
|
||
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] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
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') | ||
|
||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably not needed since ES256-R is not needed There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { verify } from '@stablelib/ed25519' | ||
import type { VerificationMethod } from 'did-resolver' | ||
import { base64ToBytes, stringToBytes } from '../util' | ||
import * as CommonVerifierAlg from './CommonVerifierAlg' | ||
|
||
export function verifyEd25519( | ||
data: string, | ||
signature: string, | ||
authenticators: VerificationMethod[] | ||
): VerificationMethod { | ||
const clear: Uint8Array = stringToBytes(data) | ||
const sig: Uint8Array = base64ToBytes(signature) | ||
const signer = authenticators.find((pk: VerificationMethod) => { | ||
return verify(CommonVerifierAlg.extractPublicKeyBytes(pk), clear, sig) | ||
}) | ||
if (!signer) throw new Error('invalid_signature: Signature invalid for JWT') | ||
return signer | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.