Skip to content

Commit

Permalink
some nice fixes
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Jan 28, 2024
1 parent bdf2ca7 commit f2ea107
Show file tree
Hide file tree
Showing 17 changed files with 430 additions and 191 deletions.
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@sd-jwt/core": "^0.2.0",
"@digitalcredentials/jsonld": "^5.2.1",
"@digitalcredentials/jsonld-signatures": "^9.3.1",
"@digitalcredentials/vc": "^1.1.2",
"@multiformats/base-x": "^4.0.1",
"@sd-jwt/core": "^0.2.0",
"@sd-jwt/decode": "^0.2.0",
"@sphereon/pex": "^3.0.1",
"@sphereon/pex-models": "^2.1.5",
"@sphereon/ssi-types": "^0.18.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { injectable } from 'tsyringe'

import { getJwkFromKey } from '../../crypto'
import { AriesFrameworkError } from '../../error'
import { Hasher, JsonTransformer, TypedArrayEncoder } from '../../utils'
import { Hasher, JsonTransformer } from '../../utils'
import { DidsApi, getKeyFromVerificationMethod } from '../dids'
import { SdJwtVcApi } from '../sd-jwt-vc'
import {
Expand Down Expand Up @@ -214,12 +214,10 @@ export class DifPresentationExchangeService {
}

return {
verifiablePresentations: await Promise.all(
verifiablePresentationResultsWithFormat.map((resultWithFormat) =>
getVerifiablePresentationFromEncoded(
agentContext,
resultWithFormat.verifiablePresentationResult.verifiablePresentation
)
verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) =>
getVerifiablePresentationFromEncoded(
agentContext,
resultWithFormat.verifiablePresentationResult.verifiablePresentation
)
),
presentationSubmission,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,24 @@ export async function getCredentialsForRequest(
...selectResultsRaw,
// Map the encoded credential to their respective w3c credential record
verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded) => {
const credentialRecordIndex = encodedCredentials.findIndex((encoded) => deepEquality(selectedEncoded, encoded))
const credentialRecordIndex = encodedCredentials.findIndex((encoded) => {
if (
typeof selectedEncoded === 'string' &&
selectedEncoded.includes('~') &&
typeof encoded === 'string' &&
encoded.includes('~')
) {
// FIXME: pex applies SD-JWT, so we actually can't match the record anymore :(
// We take the first part of the sd-jwt, as that will never change, and should
// be unique on it's own
const [encodedJwt] = encoded.split('~')
const [selectedEncodedJwt] = selectedEncoded.split('~')

return encodedJwt === selectedEncodedJwt
} else {
return deepEquality(selectedEncoded, encoded)
}
})

if (credentialRecordIndex === -1) {
throw new DifPresentationExchangeError('Unable to find credential in credential records.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function getSphereonOriginalVerifiablePresentation(
}

// TODO: we might want to move this to some generic vc transformation util
export async function getVerifiablePresentationFromEncoded(
export function getVerifiablePresentationFromEncoded(
agentContext: AgentContext,
encodedVerifiablePresentation: string | W3cJsonPresentation | SphereonW3CVerifiablePresentation
) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export class SdJwtVcApi {
/**
* Get and validate a sd-jwt-vc from a serialized JWT.
*/
public async fromCompact<Header extends SdJwtVcHeader, Payload extends SdJwtVcPayload>(sdJwtVcCompact: string) {
return await this.sdJwtVcService.fromCompact<Header, Payload>(sdJwtVcCompact)
public fromCompact<Header extends SdJwtVcHeader, Payload extends SdJwtVcPayload>(sdJwtVcCompact: string) {
return this.sdJwtVcService.fromCompact<Header, Payload>(sdJwtVcCompact)
}

public async store(compactSdJwtVc: string) {
Expand Down
66 changes: 36 additions & 30 deletions packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { Query } from '../../storage/StorageService'
import type { Signer, SdJwtVcVerificationResult, Verifier, HasherAndAlgorithm, DisclosureItem } from '@sd-jwt/core'

import { KeyBinding, SdJwtVc as _SdJwtVc, HasherAlgorithm } from '@sd-jwt/core'
import { decodeSdJwtVc } from '@sd-jwt/decode'
import { injectable } from 'tsyringe'

import { Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto'
Expand Down Expand Up @@ -93,22 +94,17 @@ export class SdJwtVcService {
} satisfies SdJwtVc<typeof header, Payload>
}

public async fromCompact<
Header extends SdJwtVcHeader = SdJwtVcHeader,
Payload extends SdJwtVcPayload = SdJwtVcPayload
>(compactSdJwtVc: string): Promise<SdJwtVc<Header, Payload>> {
const sdJwtVc = _SdJwtVc.fromCompact<Header, Payload>(compactSdJwtVc).withHasher(this.hasher)

if (!sdJwtVc.signature) {
throw new SdJwtVcError('A signature must be included for an sd-jwt-vc')
}
public fromCompact<Header extends SdJwtVcHeader = SdJwtVcHeader, Payload extends SdJwtVcPayload = SdJwtVcPayload>(
compactSdJwtVc: string
): SdJwtVc<Header, Payload> {
// NOTE: we use decodeSdJwtVc so we can make this method sync
const { decodedPayload, header, signedPayload } = decodeSdJwtVc(compactSdJwtVc, Hasher.hash)

return {
compact: compactSdJwtVc,
header: sdJwtVc.header,
payload: sdJwtVc.payload,

prettyClaims: await sdJwtVc.getPrettyClaims(),
header: header as Header,
payload: signedPayload as Payload,
prettyClaims: decodedPayload as Payload,
}
}

Expand Down Expand Up @@ -161,11 +157,16 @@ export class SdJwtVcService {
const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc))
const holder = await this.extractKeyFromHolderBinding(agentContext, this.parseHolderBindingFromCredential(sdJwtVc))

// FIXME: sdJwtVc library must support passing a custom jwk resolver based on the cnf claim
// or passing in the resolved key already. Currently the implementation assumes the cnf is always
// a jwk. But this won't work if we want to bind the cnf to a did
// FIXME: we currently pass in the required keys in the verification method and based on the header.typ we
// check if we need to use the issuer or holder key. Once better support in sd-jwt lib is available we can
// update this.
// See https://github.com/berendsliedrecht/sd-jwt-ts/pull/34
// See https://github.com/berendsliedrecht/sd-jwt-ts/issues/15
const verificationResult = await sdJwtVc.verify(
this.verifier(agentContext, issuer.key),
this.verifier(agentContext, {
issuer: issuer.key,
holder: holder.key,
}),
requiredClaimKeys,
holder.cnf
)
Expand Down Expand Up @@ -267,28 +268,33 @@ export class SdJwtVcService {
/**
* @todo validate the JWT header (alg)
* FIXME: also support kid (did) for cnf claim
*/
private verifier<Header extends SdJwtVcHeader = SdJwtVcHeader>(
agentContext: AgentContext,
signerKey: Key
verificationKeys: {
issuer: Key
holder: Key
}
): Verifier<Header> {
return async ({ message, signature, publicKeyJwk }) => {
let key = signerKey

if (publicKeyJwk) {
if (!('kty' in publicKeyJwk)) {
throw new SdJwtVcError(
'Key type (kty) claim could not be found in the JWK of the confirmation (cnf) claim. Only JWK is supported right now'
)
}
return async ({ message, signature, publicKeyJwk, header }) => {
const keyFromPublicKeyJwk = publicKeyJwk ? getJwkFromJson(publicKeyJwk as JwkJson).key : undefined
let key: Key
if (header.typ === 'kb+jwt') {
key = verificationKeys.holder
} else if (header.typ === 'vc+sd-jwt') {
key = verificationKeys.issuer
} else {
throw new SdJwtVcError(`Unsupported JWT type '${header.typ}'`)
}
key = getJwkFromJson(publicKeyJwk as JwkJson).key
if (keyFromPublicKeyJwk && key.fingerprint !== keyFromPublicKeyJwk.fingerprint) {
throw new SdJwtVcError('The key used to verify the signature does not match the expected key')
}
return await agentContext.wallet.verify({
signature: Buffer.from(signature),
key: key,
key,
data: TypedArrayEncoder.fromString(message),
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,5 +568,21 @@ describe('SdJwtVcService', () => {
isKeyBindingValid: true,
})
})

test('Verify did holder-bound sd-jwt-vc with disclosures and kb-jwt', async () => {
const verificationResult = await sdJwtVcService.verify<SdJwtVcHeader, { address: { country: string } }>(
agent.context,
{
compactSdJwtVc:
'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rcnpRUEJyNHB5cUM3NzZLS3RyejEzU2NoTTVlUFBic3N1UHVRWmI1dDR1S1EifQ.eyJ2Y3QiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZGVncmVlIjoiYmFjaGVsb3IiLCJjbmYiOnsia2lkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIn0sImlzcyI6ImRpZDprZXk6ejZNa3J6UVBCcjRweXFDNzc2S0t0cnoxM1NjaE01ZVBQYnNzdVB1UVpiNXQ0dUtRIiwiaWF0IjoxNzA2MjY0ODQwLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyJTSm81ME0xX3NUQWRPSjFObF82ekJzZWg3Ynd4czhhbDhleVotQl9nTXZFIiwiYTJjT2xWOXY4TUlWNTRvMVFrODdBMDRNZ0c3Q0hiaFZUN1lkb00zUnM4RSJdfQ.PrZtmLFPr8tBY0FKsv2yHJeqzds8m0Rlrof-Z36o7ksNvON3ZSrKHOD8fFDJaQ8oFJcZAnjpUS6pY9kwAgU1Ag~WyI5Mjg3NDM3NDQyMTg0ODk1NTU3OTA1NTkiLCJ1bml2ZXJzaXR5IiwiaW5uc2JydWNrIl0~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE3MDYyNjQ4NDAsIm5vbmNlIjoiODExNzMxNDIwNTMxODQ3NzAwNjM2ODUiLCJhdWQiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsIl9zZF9oYXNoIjoiSVd0cTEtOGUtLU9wWWpXa3Z1RTZrRjlaa2h5aDVfV3lOYXItaWtVT0FscyJ9.cJNnYH16lHn0PsF9tOQPofpONGoY19bQB5k6Ezux9TvQWS_Opnd-3m_fO9yKu8S0pmjyG2mu3Uzn1pUNqhL9AQ',
keyBinding: {
audience: 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9',
nonce: '81173142053184770063685',
},
}
)

expect(verificationResult.verification.isValid).toBe(true)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,6 @@ describe('sd-jwt-vc end to end test', () => {

const { compact, header, payload } = await issuer.sdJwtVc.sign({
payload: credential,
// FIXME: sd-jwt library does not support did binding for holder yet
// issuance is fine, but in verification of KB the jwk will be extracted
// from the cnf claim which will be undefined.
// holder: {
// method: 'did',
// didUrl: holderDidUrl,
// },
holder: {
method: 'jwk',
jwk: getJwkFromKey(holderKey),
Expand All @@ -115,7 +108,7 @@ describe('sd-jwt-vc end to end test', () => {
type Header = typeof header

// parse SD-JWT
const sdJwtVc = await holder.sdJwtVc.fromCompact<Header, Payload>(compact)
const sdJwtVc = holder.sdJwtVc.fromCompact<Header, Payload>(compact)
expect(sdJwtVc).toEqual({
compact: expect.any(String),
header: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,9 @@ describe('SdJwtVcRecord', () => {
expect(sdJwtVcRecord.createdAt).toBe(createdAt)
expect(sdJwtVcRecord.getTags()).toEqual({
some: 'tag',
disclosureKeys: [
'is_over_65',
'is_over_21',
'is_over_18',
'birthdate',
'email',
'region',
'country',
'given_name',
],
alg: 'EdDSA',
sdAlg: 'sha-256',
vct: 'IdentityCredential',
})
expect(sdJwtVcRecord.compactSdJwtVc).toEqual(compactSdJwtVc)
})
Expand Down Expand Up @@ -66,16 +59,9 @@ describe('SdJwtVcRecord', () => {
expect(instance.createdAt.getTime()).toBe(createdAt.getTime())
expect(instance.getTags()).toEqual({
some: 'tag',
disclosureKeys: [
'is_over_65',
'is_over_21',
'is_over_18',
'birthdate',
'email',
'region',
'country',
'given_name',
],
alg: 'EdDSA',
sdAlg: 'sha-256',
vct: 'IdentityCredential',
})
expect(instance.compactSdJwtVc).toBe(compactSdJwtVc)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AgentContext, SdJwtVc, W3cVerifiablePresentation } from '@aries-fr
import type { VerifiedAuthorizationRequest, PresentationExchangeResponseOpts } from '@sphereon/did-auth-siop'

import {
Hasher,
W3cJwtVerifiablePresentation,
parseDid,
AriesFrameworkError,
Expand All @@ -15,6 +16,7 @@ import {
W3cJsonLdVerifiablePresentation,
asArray,
DifPresentationExchangeService,
DifPresentationExchangeSubmissionLocation,
} from '@aries-framework/core'
import {
CheckLinkedDomain,
Expand Down Expand Up @@ -112,6 +114,7 @@ export class OpenId4VcSiopHolderService {
presentationDefinition: authorizationRequest.presentationDefinitions[0].definition,
challenge: nonce,
domain: clientId,
presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL,
})

presentationExchangeOptions = {
Expand Down Expand Up @@ -179,6 +182,7 @@ export class OpenId4VcSiopHolderService {
.withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18])
.withCustomResolver(getSphereonDidResolver(agentContext))
.withCheckLinkedDomain(CheckLinkedDomain.NEVER)
.withHasher(Hasher.hash)

if (openIdTokenIssuer) {
const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, openIdTokenIssuer)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
OpenId4VciCreateCredentialResponseOptions,
OpenId4VciCreateCredentialOfferOptions,
CredentialOffer,
} from './OpenId4VcIssuerServiceOptions'
import type { OpenId4VcIssuerRecordProps } from './repository'
import type { OpenId4VciCredentialOfferPayload } from '../shared'
Expand Down Expand Up @@ -70,9 +69,7 @@ export class OpenId4VcIssuerApi {
*
* @returns Object containing the payload of the credential offer and the credential offer request, which can be sent to the wallet.
*/
public async createCredentialOffer(
options: OpenId4VciCreateCredentialOfferOptions & { issuerId: string }
): Promise<CredentialOffer> {
public async createCredentialOffer(options: OpenId4VciCreateCredentialOfferOptions & { issuerId: string }) {
const { issuerId, ...rest } = options
const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId)
return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, { ...rest, issuer })
Expand Down
Loading

0 comments on commit f2ea107

Please sign in to comment.