Skip to content

Commit

Permalink
support signing with secp256k1 and ed25519 keys
Browse files Browse the repository at this point in the history
  • Loading branch information
mistermoe committed Dec 6, 2023
1 parent e551413 commit 3f7c236
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 56 deletions.
59 changes: 10 additions & 49 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/credentials/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"dependencies": {
"@sphereon/pex": "2.1.0",
"did-jwt": "^7.2.6",
"jwt-decode": "4.0.0",
"uuid": "^9.0.0"
},
"devDependencies": {
Expand All @@ -88,7 +89,7 @@
"@web/test-runner": "0.18.0",
"@web/test-runner-playwright": "0.11.0",
"@web5/common": "0.2.1",
"@web5/crypto": "0.2.2",
"@web5/crypto": "0.2.3",
"@web5/dids": "0.2.2",
"c8": "8.0.1",
"chai": "4.3.10",
Expand Down
46 changes: 41 additions & 5 deletions packages/credentials/src/verifiable-credential.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Resolvable, DIDResolutionResult} from 'did-resolver';
import type { CryptoAlgorithm, JwkParamsEcPrivate, JwkParamsOkpPrivate, Web5Crypto } from '@web5/crypto';
import type { JwtPayload, JwtHeader } from 'jwt-decode';
import type {
ICredential,
ICredentialSubject,
Expand All @@ -11,7 +13,7 @@ import { verifyJWT } from 'did-jwt';
import { DidDhtMethod, DidIonMethod, DidKeyMethod, DidResolver } from '@web5/dids';
import { SsiValidator } from './validators.js';
import { PortableDid } from '@web5/dids';
import { Jose, Ed25519 } from '@web5/crypto';
import { PrivateKeyJwk, EdDsaAlgorithm, EcdsaAlgorithm } from '@web5/crypto';

export const DEFAULT_CONTEXT = 'https://www.w3.org/2018/credentials/v1';
export const DEFAULT_VC_TYPE = 'VerifiableCredential';
Expand Down Expand Up @@ -76,6 +78,27 @@ type DecodedVcJwt = {
signature: string
}

type Signer<T extends Web5Crypto.Algorithm> = {
signer: CryptoAlgorithm,
options?: T | undefined
alg: string
crv: string
}

const secp256k1Signer: Signer<Web5Crypto.EcdsaOptions> = {
signer : new EcdsaAlgorithm(),
options : { name: 'ES256K'},
alg : 'ES256K',
crv : 'secp256k1'
};

const ed25519Signer: Signer<Web5Crypto.EdDsaOptions> = {
signer : new EdDsaAlgorithm(),
options : { name: 'EdDSA' },
alg : 'EdDSA',
crv : 'Ed25519'
};

const didResolver = new DidResolver({ didResolvers: [DidIonMethod, DidKeyMethod, DidDhtMethod] });

class TbdResolver implements Resolvable {
Expand All @@ -99,6 +122,14 @@ const tbdResolver = new TbdResolver();
export class VerifiableCredential {
constructor(public vcDataModel: VcDataModel) {}

/** supported cryptographic algorithms. keys are `${alg}:${crv}`. */
static algorithms: { [alg: string]: Signer<Web5Crypto.EcdsaOptions | Web5Crypto.EdDsaOptions> } = {
'ES256K:' : secp256k1Signer,
'ES256K:secp256k1' : secp256k1Signer,
':secp256k1' : secp256k1Signer,
'EdDSA:Ed25519' : ed25519Signer
};

get type(): string {
return this.vcDataModel.type[this.vcDataModel.type.length - 1];
}
Expand Down Expand Up @@ -294,9 +325,9 @@ function decode(jwt: string): DecodedVcJwt {

async function createJwt(createJwtOptions: CreateJwtOptions) {
const { issuerDid, subjectDid, payload } = createJwtOptions;
const privateKeyJwk = issuerDid.keySet.verificationMethodKeys![0].privateKeyJwk!;
const privateKeyJwk = issuerDid.keySet.verificationMethodKeys![0].privateKeyJwk! as JwkParamsEcPrivate | JwkParamsOkpPrivate;

const header: JwtHeaderParams = { typ: 'JWT', alg: privateKeyJwk.alg!, kid: issuerDid.document.verificationMethod![0].id };
const header: JwtHeader = { typ: 'JWT', alg: privateKeyJwk.alg, kid: issuerDid.document.verificationMethod![0].id };
const base64UrlEncodedHeader = Convert.object(header).toBase64Url();

const jwtPayload = {
Expand All @@ -310,9 +341,14 @@ async function createJwt(createJwtOptions: CreateJwtOptions) {
const toSign = `${base64UrlEncodedHeader}.${base64UrlEncodedPayload}`;
const toSignBytes = Convert.string(toSign).toUint8Array();

const { keyMaterial } = await Jose.jwkToKey({ key: privateKeyJwk });
const algorithmId = `${header.alg}:${privateKeyJwk['crv'] || ''}`;
if (!(algorithmId in VerifiableCredential.algorithms)) {
throw new Error(`Signing failed: ${algorithmId} not supported`);
}

const { signer, options } = VerifiableCredential.algorithms[algorithmId];

const signatureBytes = await Ed25519.sign({ key: keyMaterial, data: toSignBytes });
const signatureBytes = await signer.sign({ key: privateKeyJwk as PrivateKeyJwk, data: toSignBytes, algorithm: options! });
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url();

return `${toSign}.${base64UrlEncodedSignature}`;
Expand Down
20 changes: 19 additions & 1 deletion packages/credentials/tests/verifiable-credential.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('Verifiable Credential Tests', () => {

});

it('signing vc works', async () => {
it('signing with Ed25519 key works', async () => {
const subjectDid = issuerDid.did;

const vc = VerifiableCredential.create({
Expand All @@ -119,6 +119,24 @@ describe('Verifiable Credential Tests', () => {
expect(parts.length).to.equal(3);
});

it('signing with secp256k1 key works', async () => {
const did = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' });

const vc = VerifiableCredential.create({
type : 'StreetCred',
issuer : did.did,
subject : did.did,
data : new StreetCredibility('high', true),
});

const vcJwt = await vc.sign({ did });
expect(vcJwt).to.not.be.null;
expect(vcJwt).to.be.a('string');

const parts = vcJwt.split('.');
expect(parts.length).to.equal(3);
});

it('parseJwt throws ParseException if argument is not a valid JWT', () => {
expect(() => {
VerifiableCredential.parseJwt('hi');
Expand Down

0 comments on commit 3f7c236

Please sign in to comment.