Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-needles-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@animo-id/mdoc": patch
---

fix: always true statement throws error. In the DeviceResponse model there was an always true if statement that throws an error before allowing the creation of the response.
3 changes: 2 additions & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ export interface MdocContext {
alg: string
}) => MaybePromise<CoseKey>

validateCertificateChain: (input: {
verifyCertificateChain: (input: {
trustedCertificates: Uint8Array[]
x5chain: Uint8Array[]
now?: Date
}) => MaybePromise<void>

getCertificateData: (input: { certificate: Uint8Array }) => MaybePromise<{
Expand Down
2 changes: 1 addition & 1 deletion src/cose/sign1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class Sign1 extends CborStructure {
this.signature = signature
}

public async verify(options: { key?: CoseKey }, ctx: Pick<MdocContext, 'cose' | 'x509'>) {
public async verifySignature(options: { key?: CoseKey }, ctx: Pick<MdocContext, 'cose' | 'x509'>) {
const publicKey =
options.key ??
(await ctx.x509.getPublicKey({
Expand Down
11 changes: 7 additions & 4 deletions src/holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { base64url } from 'jose'
import type { MdocContext } from './context'
import type { CoseKey } from './cose'
import {
type DeviceNamespaces,
DeviceRequest,
DeviceResponse,
type Document,
Expand All @@ -17,7 +18,7 @@ export class Holder {
* string should be base64url encoded as defined in openid4vci Draft 15
*
*/
public static async validateIssuerSigned(
public static async verifyIssuerSigned(
options: {
issuerSigned: Uint8Array | string | IssuerSigned
verificationCallback?: VerificationCallback
Expand All @@ -34,10 +35,10 @@ export class Holder {
? IssuerSigned.decode(options.issuerSigned)
: options.issuerSigned

await issuerSigned.issuerAuth.validate(options, ctx)
await issuerSigned.issuerAuth.verify(options, ctx)
}

public static async validateDeviceRequest(
public static async verifyDeviceRequest(
options: {
deviceRequest: Uint8Array | DeviceRequest
sessionTranscript: Uint8Array | SessionTranscript
Expand All @@ -56,7 +57,7 @@ export class Holder {
: SessionTranscript.decode(options.sessionTranscript)

for (const docRequest of deviceRequest.docRequests) {
await docRequest.readerAuth?.validate(
await docRequest.readerAuth?.verify(
{
readerAuthentication: {
itemsRequest: docRequest.itemsRequest,
Expand All @@ -74,6 +75,7 @@ export class Holder {
deviceRequest: DeviceRequest
sessionTranscript: SessionTranscript
documents: Array<Document>
deviceNamespaces?: DeviceNamespaces
mac?: {
ephemeralKey: CoseKey
signingKey: CoseKey
Expand All @@ -92,6 +94,7 @@ export class Holder {
presentationDefinition: PresentationDefinition
sessionTranscript: SessionTranscript
documents: Array<Document>
deviceNamespaces?: DeviceNamespaces
mac?: {
ephemeralKey: CoseKey
signingKey: CoseKey
Expand Down
2 changes: 1 addition & 1 deletion src/mdoc/models/device-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class DeviceAuth extends CborStructure {
throw new MdlError('unreachable')
}

public async validate(
public async verify(
options: {
document: Document
verificationCallback?: VerificationCallback
Expand Down
52 changes: 32 additions & 20 deletions src/mdoc/models/device-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { EitherSignatureOrMacMustBeProvidedError } from '../errors'
import { DeviceAuth, type DeviceAuthOptions } from './device-auth'
import { DeviceAuthentication } from './device-authentication'
import { DeviceMac } from './device-mac'
import type { DeviceNamespaces } from './device-namespaces'
import { DeviceNamespaces } from './device-namespaces'
import type { DeviceRequest } from './device-request'
import { DeviceSignature } from './device-signature'
import { DeviceSigned } from './device-signed'
import type { DocRequest } from './doc-request'
import { Document, type DocumentStructure } from './document'
import { DocumentError, type DocumentErrorStructure } from './document-error'
import type { IssuerSigned } from './issuer-signed'
import type { IssuerNamespace } from './issuer-namespace'
import { IssuerSigned } from './issuer-signed'
import {
findMdocMatchingDocType,
limitDisclosureToDeviceRequestNameSpaces,
Expand Down Expand Up @@ -90,7 +91,7 @@ export class DeviceResponse extends CborStructure {
return DeviceResponse.fromEncodedStructure(structure)
}

public async validate(
public async verify(
options: {
sessionTranscript: SessionTranscript | Uint8Array
ephemeralReaderKey?: CoseKey
Expand All @@ -116,7 +117,7 @@ export class DeviceResponse extends CborStructure {
})

for (const document of this.documents ?? []) {
await document.issuerSigned.issuerAuth.validate(
await document.issuerSigned.issuerAuth.verify(
{
disableCertificateChainValidation: options.disableCertificateChainValidation,
now: options.now,
Expand All @@ -126,7 +127,7 @@ export class DeviceResponse extends CborStructure {
ctx
)

await document.deviceSigned.deviceAuth.validate(
await document.deviceSigned.deviceAuth.verify(
{
document,
ephemeralMacPrivateKey: options.ephemeralReaderKey,
Expand All @@ -139,50 +140,53 @@ export class DeviceResponse extends CborStructure {
ctx
)

await document.issuerSigned.validate({ verificationCallback: onCheck }, ctx)
await document.issuerSigned.verify({ verificationCallback: onCheck }, ctx)
}
}

private static async create(
limitDisclosureCb:
| ((issuerSigned: IssuerSigned, inputDescriptor: InputDescriptor) => DeviceNamespaces)
| ((issuerSigned: IssuerSigned, docRequest: DocRequest) => DeviceNamespaces),
| ((issuerSigned: IssuerSigned, inputDescriptor: InputDescriptor) => IssuerNamespace)
| ((issuerSigned: IssuerSigned, docRequest: DocRequest) => IssuerNamespace),
options: {
inputDescriptorsOrRequests: Array<InputDescriptor> | Array<DocRequest>
sessionTranscript: SessionTranscript
documents: Array<Document>
mac?: {
ephemeralKey: CoseKey
deviceNamespaces?: DeviceNamespaces
signature?: {
signingKey: CoseKey
}
signature?: {
mac?: {
ephemeralKey: CoseKey
signingKey: CoseKey
}
},
ctx: Pick<MdocContext, 'crypto' | 'cose'>
) {
if (!(options.mac && options.signature) || (options.mac && options.signature)) {
throw new EitherSignatureOrMacMustBeProvidedError()
}

const useMac = !!options.mac
const useSignature = !!options.signature
const signingKey = useSignature ? options.mac.signingKey : options.signature.signingKey
if (useMac === useSignature) throw new EitherSignatureOrMacMustBeProvidedError()

const signingKey = useSignature ? options.signature?.signingKey : options.mac?.signingKey
if (!signingKey) throw new Error('Signing key is missing')

const documents = await Promise.all(
options.inputDescriptorsOrRequests.map(async (idOrRequest) => {
const document = findMdocMatchingDocType(
options.documents,
'id' in idOrRequest ? idOrRequest.id : idOrRequest.itemsRequest.docType
)
const deviceNamespaces = limitDisclosureCb(
const disclosedIssuerNamespaces = limitDisclosureCb(
document.issuerSigned,
idOrRequest as unknown as InputDescriptor & DocRequest
)

const deviceNamespaces = options.deviceNamespaces ?? new DeviceNamespaces({ deviceNamespaces: new Map() })

const deviceAuthenticationBytes = new DeviceAuthentication({
sessionTranscript: options.sessionTranscript,
docType: document.docType,
deviceNamespaces: document.deviceSigned.deviceNamespaces,
deviceNamespaces,
Copy link
Member

Choose a reason for hiding this comment

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

I think this is still incorrect maybe, since we don't want to do any limit disclosure on the device namespaces (as devicenamesaces is the return value of limitDisclosureCb).

See also the 0.5 implementation: https://github.com/animo/mdoc/blob/v0.5.3-alpha-20250322020612/src/mdoc/model/device-response.ts#L353-L376

}).encode({ asDataItem: true })

const unprotectedHeaders = signingKey.keyId
Expand Down Expand Up @@ -215,10 +219,13 @@ export class DeviceResponse extends CborStructure {
detachedContent: deviceAuthenticationBytes,
})

const ephemeralKey = options.mac?.ephemeralKey
if (!ephemeralKey) throw new Error('Ephemeral key is missing')

await deviceMac.addTag(
{
privateKey: signingKey,
ephemeralKey: (options.mac as Required<typeof options.mac>).ephemeralKey,
ephemeralKey: ephemeralKey,
sessionTranscript: options.sessionTranscript,
},
ctx
Expand All @@ -229,7 +236,10 @@ export class DeviceResponse extends CborStructure {

return new Document({
docType: document.docType,
issuerSigned: document.issuerSigned,
issuerSigned: new IssuerSigned({
issuerNamespaces: disclosedIssuerNamespaces,
issuerAuth: document.issuerSigned.issuerAuth,
}),
deviceSigned: new DeviceSigned({
deviceNamespaces,
deviceAuth: new DeviceAuth(deviceAuthOptions),
Expand All @@ -248,6 +258,7 @@ export class DeviceResponse extends CborStructure {
deviceRequest: DeviceRequest
sessionTranscript: SessionTranscript
documents: Array<Document>
deviceNamespaces?: DeviceNamespaces
mac?: {
ephemeralKey: CoseKey
signingKey: CoseKey
Expand All @@ -270,6 +281,7 @@ export class DeviceResponse extends CborStructure {
presentationDefinition: PresentationDefinition
sessionTranscript: SessionTranscript
documents: Array<Document>
deviceNamespaces?: DeviceNamespaces
mac?: {
ephemeralKey: CoseKey
signingKey: CoseKey
Expand Down
9 changes: 5 additions & 4 deletions src/mdoc/models/issuer-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class IssuerAuth extends Sign1 {
return mso
}

public async validate(
public async verify(
options: {
verificationCallback?: VerificationCallback
now?: Date
Expand All @@ -54,9 +54,10 @@ export class IssuerAuth extends Sign1 {
throw new Error('No trusted certificates found. Cannot verify issuer signature.')
}

await ctx.x509.validateCertificateChain({
await ctx.x509.verifyCertificateChain({
trustedCertificates,
x5chain: this.certificateChain,
now,
})

onCheck({
Expand All @@ -72,7 +73,7 @@ export class IssuerAuth extends Sign1 {
}
}

const isSignatureValid = await this.verify({}, ctx)
const isSignatureValid = await this.verifySignature({}, ctx)

onCheck({
status: isSignatureValid ? 'PASSED' : 'FAILED',
Expand All @@ -86,7 +87,7 @@ export class IssuerAuth extends Sign1 {
})

onCheck({
status: validityInfo.validateSigned(notBefore, notAfter) ? 'FAILED' : 'PASSED',
status: validityInfo.verifySigned(notBefore, notAfter) ? 'FAILED' : 'PASSED',
check: 'The MSO signed date must be within the validity period of the certificate',
reason: `The MSO signed date (${validityInfo.signed.toUTCString()}) must be within the validity period of the certificate (${notBefore.toUTCString()} to ${notAfter.toUTCString()})`,
})
Expand Down
2 changes: 1 addition & 1 deletion src/mdoc/models/issuer-signed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class IssuerSigned extends CborStructure {
return IssuerSigned.decode(base64url.decode(encoded))
}

public async validate(
public async verify(
options: { verificationCallback?: VerificationCallback },
ctx: Pick<MdocContext, 'x509' | 'crypto'>
) {
Expand Down
61 changes: 20 additions & 41 deletions src/mdoc/models/pex-limit-disclosure.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,31 @@
import type { DataElementIdentifier } from './data-element-identifier.js'
import type { DataElementValue } from './data-element-value.js'
import { DeviceNamespaces } from './device-namespaces.js'
import { DeviceSignedItems } from './device-signed-items.js'
import type { DocRequest } from './doc-request.js'
import type { DocType } from './doctype.js'
import type { Document } from './document.js'
import type { IssuerNamespace } from './issuer-namespace.js'
import type { IssuerSignedItem } from './issuer-signed-item.js'
import type { IssuerSigned } from './issuer-signed.js'
import type { InputDescriptor } from './presentation-definition.js'
import type { DocRequest } from './doc-request'
import type { DocType } from './doctype'
import type { Document } from './document'
import { IssuerNamespace } from './issuer-namespace'
import type { IssuerSigned } from './issuer-signed'
import type { IssuerSignedItem } from './issuer-signed-item'
import type { Namespace } from './namespace'
import type { InputDescriptor } from './presentation-definition'

export const limitDisclosureToDeviceRequestNameSpaces = (
issuerSigned: IssuerSigned,
docRequest: DocRequest
): DeviceNamespaces => {
const deviceNamespaces: Map<string, DeviceSignedItems> = new Map()

for (const [nameSpace, nameSpaceFields] of docRequest.itemsRequest.namespaces.entries()) {
const nsAttrs = issuerSigned.issuerNamespaces?.issuerNamespaces.get(nameSpace) ?? []
): IssuerNamespace => {
const issuerNamespaces = new Map<Namespace, Array<IssuerSignedItem>>()
for (const [namespace, nameSpaceFields] of docRequest.itemsRequest.namespaces.entries()) {
const nsAttrs = issuerSigned.issuerNamespaces?.issuerNamespaces.get(namespace) ?? []
const issuerSignedItems = Array.from(nameSpaceFields.entries()).map(([elementIdentifier, _]) => {
const issuerSignedItem = prepareIssuerSignedItem(elementIdentifier, nsAttrs)

if (!issuerSignedItem) {
throw new Error(`No matching field found for '${elementIdentifier}'`)
}

return issuerSignedItem
})

const deviceSignedItems = new Map<DataElementIdentifier, DataElementValue>()

for (const issuerSignedItem of issuerSignedItems) {
deviceSignedItems.set(issuerSignedItem.elementIdentifier, issuerSignedItem.elementValue)
}

deviceNamespaces.set(nameSpace, new DeviceSignedItems({ deviceSignedItems }))
issuerNamespaces.set(namespace, issuerSignedItems)
}

return new DeviceNamespaces({ deviceNamespaces })
return new IssuerNamespace({ issuerNamespaces })
}

const prepareIssuerSignedItem = (
Expand Down Expand Up @@ -144,8 +132,8 @@ export const findMdocMatchingDocType = (documents: Array<Document>, docType: Doc
export const limitDisclosureToInputDescriptor = (
issuerSigned: IssuerSigned,
inputDescriptor: InputDescriptor
): DeviceNamespaces => {
const deviceNamespaces: Map<string, DeviceSignedItems> = new Map()
): IssuerNamespace => {
const issuerNamespaces = new Map<Namespace, Array<IssuerSignedItem>>()

for (const field of inputDescriptor.constraints.fields) {
const result = prepareDigestForInputDescriptor(field.path, issuerSigned.issuerNamespaces)
Expand All @@ -161,18 +149,9 @@ export const limitDisclosureToInputDescriptor = (
}

const { namespace, digest } = result
const entry = deviceNamespaces.get(namespace)
if (!entry) {
deviceNamespaces.set(namespace, issuerSignedItemToDeviceSignedItems(digest))
} else {
entry.deviceSignedItems.set(digest.elementIdentifier, digest.elementValue)
}
const entry = issuerNamespaces.get(namespace)
if (!entry) issuerNamespaces.set(namespace, [digest])
else entry.push(digest)
}

return new DeviceNamespaces({ deviceNamespaces })
return new IssuerNamespace({ issuerNamespaces })
}

const issuerSignedItemToDeviceSignedItems = (issuerSignedItem: IssuerSignedItem) =>
new DeviceSignedItems({
deviceSignedItems: new Map([[issuerSignedItem.elementIdentifier, issuerSignedItem.elementValue]]),
})
Loading
Loading