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

feat: mdoc-support #2054

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

auer-martin
Copy link
Contributor

No description provided.

Copy link

changeset-bot bot commented Oct 8, 2024

🦋 Changeset detected

Latest commit: 95eb0db

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@credo-ts/core Patch
@credo-ts/action-menu Patch
@credo-ts/anoncreds Patch
@credo-ts/askar Patch
@credo-ts/bbs-signatures Patch
@credo-ts/cheqd Patch
@credo-ts/drpc Patch
@credo-ts/indy-sdk-to-askar-migration Patch
@credo-ts/indy-vdr Patch
@credo-ts/node Patch
@credo-ts/openid4vc Patch
@credo-ts/question-answer Patch
@credo-ts/react-native Patch
@credo-ts/tenants Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Signed-off-by: Martin Auer <[email protected]>
Signed-off-by: Martin Auer <[email protected]>
Comment on lines 117 to 119
responseUri:
authorizationRequest.authorizationRequestPayload.response_uri ??
authorizationRequest.authorizationRequestPayload.response_uri,
Copy link
Contributor

Choose a reason for hiding this comment

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

this is redundant

@@ -109,6 +110,14 @@ export class OpenId4VcSiopHolderService {
challenge: nonce,
domain: clientId,
presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL,
openid4vp: {
Copy link
Contributor

Choose a reason for hiding this comment

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

how are these values mapped to the presentation? Ideally we don't add oid4vp specific properties to the pex service, but i can understand if it's not possible otherwise

Copy link
Contributor Author

@auer-martin auer-martin Oct 8, 2024

Choose a reason for hiding this comment

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

Yeah, I also don't like it, but I don't see an easy better way to propagate the values. There are used to calculate the session transcript

@@ -109,6 +110,14 @@ export class OpenId4VcSiopHolderService {
challenge: nonce,
domain: clientId,
presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL,
openid4vp: {
clientId,
verifierGeneratedNonce: nonce,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be extracted from the challenge property in the pex service itself. As it's now redundant, and can lead to inconsistencies.

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 did this intentionally. I don't think we should use a challenge property. openid4vp is very specific. To me it would be completely unclear why the verification of a device response will fail after changing a e.g. domain/challenge property which have no relation at all to Mdoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think?

openid4vp: {
clientId,
verifierGeneratedNonce: nonce,
mdocGeneratedNonce: await agentContext.wallet.generateNonce(),
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to make sure to reuse this nonce when encrypting the resposne (we generate a new nonce there at the moment, but these should be the same)

@@ -41,6 +45,8 @@ export function getSphereonVerifiablePresentation(
return JsonTransformer.toJSON(verifiablePresentation) as SphereonW3cVerifiablePresentation
} else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) {
return verifiablePresentation.serializedJwt
} else if (verifiablePresentation instanceof MdocVerifiablePresentation) {
throw new CredoError('Mdoc verifiable credential is not yet supported.')
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw new CredoError('Mdoc verifiable credential is not yet supported.')
throw new CredoError('Mdoc verifiable presentation is not yet supported.')


const { deviceResponseBase64Url, presentationSubmission } = await MdocDeviceResponse.openId4Vp(agentContext, {
mdocs: [Mdoc.fromBase64Url(mdocRecord.base64Url)],
presentationDefinition: presentationDefinition as DifPresentationExchangeDefinitionV2,
Copy link
Contributor

Choose a reason for hiding this comment

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

what if v1 is used?

claimFormat: presentationToCreate.claimFormat,
})

continue
Copy link
Contributor

Choose a reason for hiding this comment

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

I think instead of continue we could use an else clause maybe? I had a hard time understanding this code, until i spotted this continue statement

Comment on lines 34 to 35
.filter((c) => c instanceof MdocRecord === false)
.map((c) => getSphereonOriginalVerifiableCredential(c as SdJwtVcRecord | W3cCredentialRecord))
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.filter((c) => c instanceof MdocRecord === false)
.map((c) => getSphereonOriginalVerifiableCredential(c as SdJwtVcRecord | W3cCredentialRecord))
.filter((c): c is Exclude<typeof c, MdocRecord> => c instanceof MdocRecord === false)
.map((c) => getSphereonOriginalVerifiableCredential(c))

...selectResultsRaw,
areRequiredCredentialsPresent:
mdocPresentationDefinition.input_descriptors.length >= 1
? 'warn' // we don't know yet wheater the required credentials are present
Copy link
Contributor

Choose a reason for hiding this comment

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

when will we know this? (i don't see warn changed to error / info?)

Comment on lines +53 to +59
public get deviceSignedNamespaces(): MdocNameSpaces {
if (this.issuerSignedDocument instanceof DeviceSignedDocument === false) {
throw new MdocError(`Cannot get 'device-namespaces from a IssuerSignedDocument. Must be a DeviceSignedDocument.`)
}

return this.issuerSignedDocument.allDeviceSignedNamespaces
}
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't device signed the presentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes. It can also include device-signed namespaces. Essentially self-attested claims.

document.addIssuerNameSpace(namespace, namespaceRecord)
}

const cert = X509Certificate.fromEncodedCertificate(issuerCertificate)
Copy link
Contributor

Choose a reason for hiding this comment

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

i see we're mostly using string certificates here. Do we want to use that as the primary method to pass around certificate in Credo (i haven't made my mind up yet, just thinking 🤔 ).

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 don't mind. We essentially also set trustedCerts as strings. That's why I have chosen this approach

export interface X509ModuleConfigOptions {
  /**
   *
   * Array of trusted base64-encoded certificate strings in the DER-format.
   */
  trustedCertificates?: [string, ...string[]]

}

const cert = X509Certificate.fromEncodedCertificate(issuerCertificate)
const issuerPrivateJwk = await getJwkFromKey(options.issuerKey ?? cert.publicKey)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we check if the issuerKey and cert key match? Can't we always just extract the key from the cert?

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 also think this should work. Not sure why it doesn't. It may be related to the issue I mentioned below with the jwk representation of keys.

const issuerPrivateJwk = await getJwkFromKey(options.issuerKey ?? cert.publicKey)
const issuerSignedDocument = await document.sign(
{
issuerPrivateKey: issuerPrivateJwk.toJson(),
Copy link
Contributor

Choose a reason for hiding this comment

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

this is the public key jwk, not the private key jwk right? I think the name is confusing. It's important we don't have to pass the private key, so we can support HSM

const issuerSignedDocument = await document.sign(
{
issuerPrivateKey: issuerPrivateJwk.toJson(),
alg: issuerPrivateJwk.supportedSignatureAlgorithms[0] as 'ES256' | 'ES384' | 'ES512' | 'EdDSA',
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the casting here, it can lead to bugs in the future. We should check if index 0 exists, and then also whether it's a supported alg by mdoc (or does it throw if the provided alg is not supported?)

return new Mdoc(issuerSignedDocument)
}

public async verify(agentContext: AgentContext, options?: MdocVerifyOptions): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

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

just as an FYI trusted certificaes will need to be aligned with #2052 once merged

getMdocContext(agentContext)
)

await verifier.verifyData({ mdoc: this.issuerSignedDocument }, mdocContext)
Copy link
Contributor

Choose a reason for hiding this comment

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

does it return a verification object or something? Would be good to return more than true/false, but also something we can add later

* @param options {MdocCreateOptions}
* @returns {Promise<Mdoc>}
*/
public async create(options: MdocCreateOptions) {
Copy link
Contributor

Choose a reason for hiding this comment

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

does create also sign? I think in SD JWT API we use sign so might be nice to stay consistent?

Comment on lines 56 to 58
const { mac0, jwk, options } = input
const { data, signature, alg } = mac0.getRawVerificationData(options)
return await verifyWithJwk({ jwk, signature, data, alg })
Copy link
Contributor

Choose a reason for hiding this comment

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

does this still use the credo wallet to verify the signature? I can't see the context or something passed around. It is important.

Aslo it's prob ok for now, but we should try to stay away from doing crypto operations outside of the wallet. Also with e.g. the p256 shared secret calcuation etc.. The more we can do in the wallet, the better.

So if you can provide a list of all cryptographic operations that are needed, we can look to move them to the wallet over time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We just need the key derivation.

},
getPublicKey: async (input) => {
const certificate = new x509.X509Certificate(input.certificate)
const key = await importX509({
Copy link
Contributor

Choose a reason for hiding this comment

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

what do these do? and why are they part of the protokol library?

Copy link
Contributor Author

@auer-martin auer-martin Oct 8, 2024

Choose a reason for hiding this comment

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

Please take a look at my comment. There is an issue with how we represent the key as jwk. It probably should be compressed. Nothing from the protokoll library should remain here. Atleast related to crypto.

Copy link
Contributor

Choose a reason for hiding this comment

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

Which comment? I can't find it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

    //expected
    //{
    //kty: 'EC',
    //x: 'OFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1E',
    //y: 'EyIAXV8gyt5FcRsYHhz4ryz97rjL0uogxHO6jMZr3bg',
    //crv: 'P-256'
    //}

    //actual
    //{
    //kty: 'EC',
    //crv: 'P-256',
    //x: 'OFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPiv',
    //y: 'LP3uuMvS6iDEc7qMxmvduNeBp_oWscK1x-3_1KKYDayIctdDcpXHi8HcbehAfVIK'
    //}

    //const comp = X509Certificate.fromRawCertificate(input.certificate)
    //const x = getJwkFromKey(comp.publicKey).toJson()
    //// eslint-disable-next-line @typescript-eslint/no-unused-vars
    //const { use, ...jwk } = x
    //return jwk


const publicDeviceJwk = COSEKey.import(deviceKeyInfo.deviceKey).toJWK()

deviceKeyInfo.deviceKey
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
deviceKeyInfo.deviceKey

return {
deviceResponseBase64Url: TypedArrayEncoder.toBase64URL(deviceResponseMdoc.encode()),
presentationSubmission: MdocDeviceResponse.createPresentationSubmission({
id: 'MdocPresentationSubmission ' + agentContext.wallet.generateNonce(),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can also just use an uuid?

Suggested change
id: 'MdocPresentationSubmission ' + agentContext.wallet.generateNonce(),
id: 'MdocPresentationSubmission ' + await agentContext.wallet.generateNonce(),

Comment on lines +165 to +169
const trustedCerts =
options?.trustedCertificates ?? agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates

if (!trustedCerts) {
throw new MdocError('No trusted certificates found. Cannot verify mdoc.')
Copy link
Contributor

Choose a reason for hiding this comment

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

same with integraiont of pr #2052

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants