Skip to content

Commit

Permalink
Merge branch 'main' of github.com:openwallet-foundation/credo-ts
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Oct 8, 2024
2 parents a91f82b + 1d83159 commit f713a59
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<br />
<img
alt="Credo Logo"
src="https://github.com/openwallet-foundation/credo-ts/blob/c7886cb8377ceb8ee4efe8d264211e561a75072d/images/credo-logo.png"
src="https://raw.githubusercontent.com/openwallet-foundation/credo-ts/c7886cb8377ceb8ee4efe8d264211e561a75072d/images/credo-logo.png"
height="250px"
/>
</p>
Expand Down
18 changes: 10 additions & 8 deletions packages/askar/src/wallet/AskarBaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,10 +502,10 @@ export abstract class AskarBaseWallet implements Wallet {
epk: ephemeralKey.jwkPublic,
}

const encodedHeader = JsonEncoder.toBuffer(_header)
const encodedHeader = JsonEncoder.toBase64URL(_header)

const ecdh = new EcdhEs({
algId: Uint8Array.from(Buffer.from(encAlg)),
algId: Uint8Array.from(Buffer.from(encryptionAlgorithm)),
apu: apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(apu)) : Uint8Array.from([]),
apv: apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(apv)) : Uint8Array.from([]),
})
Expand All @@ -518,12 +518,13 @@ export abstract class AskarBaseWallet implements Wallet {
algorithm: keyAlgFromString(recipientKey.keyType),
publicKey: recipientKey.publicKey,
}),
aad: Uint8Array.from(encodedHeader),
// NOTE: aad is bytes of base64url encoded string. It SHOULD NOT be decoded as base64
aad: Uint8Array.from(Buffer.from(encodedHeader)),
})

const compactJwe = `${TypedArrayEncoder.toBase64URL(encodedHeader)}..${TypedArrayEncoder.toBase64URL(
nonce
)}.${TypedArrayEncoder.toBase64URL(ciphertext)}.${TypedArrayEncoder.toBase64URL(tag)}`
const compactJwe = `${encodedHeader}..${TypedArrayEncoder.toBase64URL(nonce)}.${TypedArrayEncoder.toBase64URL(
ciphertext
)}.${TypedArrayEncoder.toBase64URL(tag)}`
return compactJwe
}

Expand Down Expand Up @@ -569,7 +570,7 @@ export abstract class AskarBaseWallet implements Wallet {
const encAlg = KeyAlgs.AesA256Gcm

const ecdh = new EcdhEs({
algId: Uint8Array.from(Buffer.from(encAlg)),
algId: Uint8Array.from(Buffer.from(header.enc)),
apu: header.apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apu)) : Uint8Array.from([]),
apv: header.apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apv)) : Uint8Array.from([]),
})
Expand All @@ -581,7 +582,8 @@ export abstract class AskarBaseWallet implements Wallet {
ephemeralKey: Jwk.fromJson(header.epk),
recipientKey: askarKey,
tag: TypedArrayEncoder.fromBase64(encodedTag),
aad: TypedArrayEncoder.fromBase64(encodedHeader),
// NOTE: aad is bytes of base64url encoded string. It SHOULD NOT be decoded as base64
aad: TypedArrayEncoder.fromString(encodedHeader),
})

return { data: Buffer.from(plaintext), header }
Expand Down
36 changes: 35 additions & 1 deletion packages/askar/src/wallet/__tests__/AskarWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SignOptions,
VerifyOptions,
} from '@credo-ts/core'
import type { JwkProps } from '@hyperledger/aries-askar-shared'

import {
WalletKeyExistsError,
Expand All @@ -21,7 +22,10 @@ import {
Buffer,
JsonEncoder,
} from '@credo-ts/core'
import { Store } from '@hyperledger/aries-askar-shared'
import { Key as AskarKey } from '@hyperledger/aries-askar-nodejs'
import { Jwk, Store } from '@hyperledger/aries-askar-shared'
import { readFileSync } from 'fs'
import path from 'path'

import { KeyBackend } from '../../../../core/src/crypto/KeyBackend'
import { encodeToBase58 } from '../../../../core/src/utils/base58'
Expand Down Expand Up @@ -211,6 +215,36 @@ describe('AskarWallet basic operations', () => {
})
expect(JsonEncoder.fromBuffer(data)).toEqual({ vp_token: ['something'] })
})

test('decrypt using JWE ECDH-ES based on test vector from OpenID Conformance test', async () => {
const {
compactJwe,
decodedPayload,
privateKeyJwk,
header: expectedHeader,
} = JSON.parse(
readFileSync(path.join(__dirname, '__fixtures__/jarm-jwe-encrypted-response.json')).toString('utf-8')
) as {
compactJwe: string
decodedPayload: Record<string, unknown>
privateKeyJwk: JwkProps
header: string
}

const key = AskarKey.fromJwk({ jwk: Jwk.fromJson(privateKeyJwk) })
const recipientKey = await askarWallet.createKey({
keyType: KeyType.P256,
privateKey: Buffer.from(key.secretBytes),
})

const { data, header } = await askarWallet.directDecryptCompactJweEcdhEs({
compactJwe,
recipientKey,
})

expect(header).toEqual(expectedHeader)
expect(JsonEncoder.fromBuffer(data)).toEqual(decodedPayload)
})
})

describe.skip('Currently, all KeyTypes are supported by Askar natively', () => {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export class DifPresentationExchangeService {
})

return {
verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) =>
verifiablePresentations: verifiablePresentationResultsWithFormat.flatMap((resultWithFormat) =>
getVerifiablePresentationFromEncoded(
agentContext,
resultWithFormat.verifiablePresentationResult.verifiablePresentation
Expand Down
10 changes: 5 additions & 5 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "link:../../../Code/OID4VC/packages/siop-oid4vp",
"@sphereon/oid4vc-common": "0.16.1-unstable.68",
"@sphereon/oid4vci-client": "0.16.1-unstable.68",
"@sphereon/oid4vci-common": "0.16.1-unstable.68",
"@sphereon/oid4vci-issuer": "0.16.1-unstable.68",
"@sphereon/did-auth-siop": "0.16.1-next.66",
"@sphereon/oid4vc-common": "0.16.1-next.66",
"@sphereon/oid4vci-client": "0.16.1-next.66",
"@sphereon/oid4vci-common": "0.16.1-next.66",
"@sphereon/oid4vci-issuer": "0.16.1-next.66",
"@sphereon/ssi-types": "0.29.1-unstable.121",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,23 @@ import type {
} from './OpenId4vcSiopHolderServiceOptions'
import type { OpenId4VcJwtIssuer } from '../shared'
import type { AgentContext, JwkJson, VerifiablePresentation } from '@credo-ts/core'
import type {
AuthorizationResponsePayload,
PresentationExchangeResponseOpts,
RequestObjectPayload,
VerifiedAuthorizationRequest,
} from '@sphereon/did-auth-siop'
import type { PresentationExchangeResponseOpts, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop'

import {
Buffer,
CredoError,
DifPresentationExchangeService,
DifPresentationExchangeSubmissionLocation,
MdocVerifiablePresentation,
Hasher,
W3cJsonLdVerifiablePresentation,
W3cJwtVerifiablePresentation,
asArray,
getJwkFromJson,
injectable,
parseDid,
MdocVerifiablePresentation,
getJwkFromJson,
KeyType,
TypedArrayEncoder,
} from '@credo-ts/core'
import { OP, ResponseIss, ResponseMode, ResponseType, SupportedVersion, VPTokenLocation } from '@sphereon/did-auth-siop'

Expand Down Expand Up @@ -182,10 +179,17 @@ export class OpenId4VcSiopHolderService {
throw new CredoError('Only encrypted JARM responses are supported.')
}

// Extract nonce from the request, we use this as the `apv`
const nonce = authorizationRequest.payload?.nonce
if (!nonce || typeof nonce !== 'string') {
throw new CredoError('Missing nonce in authorization request payload')
}

const jwe = await this.encryptJarmResponse(agentContext, {
jwkJson: { ...jwk, kty: jwk.kty },
jwkJson: jwk as JwkJson,
payload: authorizationResponsePayload,
encryptionAlgorithm: validatedMetadata.client_metadata.authorization_encrypted_response_alg,
authorizationRequestNonce: nonce,
alg: validatedMetadata.client_metadata.authorization_encrypted_response_alg,
enc: validatedMetadata.client_metadata.authorization_encrypted_response_enc,
})

Expand Down Expand Up @@ -334,7 +338,13 @@ export class OpenId4VcSiopHolderService {

private async encryptJarmResponse(
agentContext: AgentContext,
options: { jwkJson: JwkJson; payload: Record<string, unknown>; encryptionAlgorithm: string; enc: string }
options: {
jwkJson: JwkJson
payload: Record<string, unknown>
alg: string
enc: string
authorizationRequestNonce: string
}
) {
const { payload, jwkJson } = options
const jwk = getJwkFromJson(jwkJson)
Expand All @@ -346,16 +356,28 @@ export class OpenId4VcSiopHolderService {
)
}

if (options.alg !== 'ECDH-ES') {
throw new CredoError("Only 'ECDH-ES' is supported as 'alg' value for JARM response encryption")
}

if (options.enc !== 'A256GCM') {
throw new CredoError("Only 'A256GCM' is supported as 'enc' value for JARM response encryption")
}

if (key.keyType !== KeyType.P256) {
throw new CredoError(`Only '${KeyType.P256}' key type is supported for JARM response encryption`)
}

const data = Buffer.from(JSON.stringify(payload))
const jwe = await agentContext.wallet.directEncryptCompactJweEcdhEs({
data,
recipientKey: key,
header: {
alg: jwkJson.alg,
kid: jwkJson.kid,
enc: 'A256GCM',
},
encryptionAlgorithm: 'A256GCM',
encryptionAlgorithm: options.enc,
apu: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(await agentContext.wallet.generateNonce())),
apv: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(options.authorizationRequestNonce)),
})

return jwe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,20 @@ export class OpenId4VcSiopVerifierService {
state = await authorizationResponseInstance.getMergedProperty<string>('state', {
hasher: Hasher.hash,
})

if (!nonce || !state) {
throw new CredoError(
'Could not extract nonce or state from authorization response. Unable to find OpenId4VcVerificationSession.'
)
}
} else {
nonce = authorizationResponseParams.nonce
state = authorizationResponse
if (authorizationResponseParams?.nonce && !authorizationResponseParams?.state) {
throw new CredoError(
'Either nonce or state must be provided if no authorization response is provided. Unable to find OpenId4VcVerificationSession.'
)
}
nonce = authorizationResponseParams?.nonce
state = authorizationResponseParams?.state
}

const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(agentContext, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ async function getVerificationSession(
options: {
verifierId: string
state?: string
nonce?: unknown
nonce?: string
}
): Promise<OpenId4VcVerificationSessionRecord> {
const { verifierId, state, nonce } = options

const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService)
const session = await openId4VcVerifierService.findVerificationSessionForAuthorizationResponse(agentContext, {
authorizationResponseParams: { state, nonce: nonce as string },
authorizationResponseParams: { state, nonce },
verifierId,
})

Expand Down Expand Up @@ -80,7 +80,7 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc
verificationSession = await getVerificationSession(agentContext, {
verifierId: verifier.verifierId,
state: input.state,
nonce: input.nonce,
nonce: input.nonce as string,
})

const req = await AuthorizationRequest.fromUriOrJwt(verificationSession.authorizationRequestJwt)
Expand All @@ -93,6 +93,7 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc
decryptCompact: decryptJarmResponse(agentContext),
})

// FIXME: verify the apv matches the nonce of the authorization reuqest
authorizationResponsePayload = res.authResponseParams as AuthorizationResponsePayload
} else {
authorizationResponsePayload = request.body
Expand Down
Loading

0 comments on commit f713a59

Please sign in to comment.