mdoc

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 20 Imported by: 0

README

go-mdoc

ISO/IEC 18013-5 mdoc (CBOR/COSE) for EUDI Wallet relying parties:

  • Parse and verify a DeviceResponse for remote flows (ISO/IEC TS 18013-7 / OpenID4VP Annex B): MSO / issuer-data authentication, issuer-data integrity, and mdoc (device) authentication bound to a session transcript.
  • SessionTranscript constructors for the OpenID4VP and DC-API handovers.
  • A minimal Issue / DevicePresent façade for test wallets and a future issuer.
  • Framework-free (no Azugo/platform-kit); all COSE/X.509 crypto is delegated to go-eudi-crypto (ECCG-pinned policy). Trust-agnostic: the issuer certificate chain is resolved through a caller-supplied IssuerChainResolver callback.
  • deviceMac (session-encryption MAC) is out of scope; remote flows use the device signature.

Implemented specs: ISO/IEC 18013-5:2021 §8.3/§9.1, ISO/IEC TS 18013-7:2024 Annex B, OpenID4VP 1.0 Annex B.2, ARF 2.9. See SPECREFS.md.

Status: pre-v1. API frozen no earlier than an OIDF/ISO conformance pass.

Documentation

Overview

Package mdoc implements ISO/IEC 18013-5 mdoc structures (CBOR/COSE) and the verification of a DeviceResponse for remote flows (ISO/IEC TS 18013-7 / OpenID4VP Annex B): MSO / issuer-data authentication, issuer-data integrity, and device authentication bound to a SessionTranscript. A minimal Issue / DevicePresent façade supports test wallets and a future issuer.

The package is framework-free (ADR-0004): it does no logging, takes a context.Context and an IssuerChainResolver callback for trust, and delegates every COSE/X.509 operation to go-eudi-crypto (ECCG-pinned policy, hard rule 4). It never names a COSE/JWS algorithm; the sole MSO digest-algorithm allow-list (digest.go) is flagged for migration into go-eudi-crypto.

deviceMac (session-encryption MAC) is out of scope for remote presentment (WP-03 Decisions).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMalformed: CBOR structurally invalid or violates the hardened decode
	// options. Maps to err:credential:parse.
	ErrMalformed = errors.New("mdoc: malformed")
	// ErrUnsupported: a well-formed but unsupported construct (e.g. version).
	ErrUnsupported = errors.New("mdoc: unsupported")
	// ErrIssuerAuth: IssuerAuth (MSO) signature or chain resolution failed.
	// Maps to err:credential:issuer-untrusted.
	ErrIssuerAuth = errors.New("mdoc: issuer authentication failed")
	// ErrIntegrity: an IssuerSignedItem digest does not match MSO ValueDigests,
	// or an item is absent from ValueDigests. Maps to err:credential:integrity.
	ErrIntegrity = errors.New("mdoc: issuer-data integrity check failed")
	// ErrDeviceAuth: DeviceAuth signature invalid / not bound to the transcript.
	// Maps to err:credential:binding-failed.
	ErrDeviceAuth = errors.New("mdoc: device authentication failed")
	// ErrValidity: current time outside the MSO ValidityInfo window.
	// Maps to err:credential:expired.
	ErrValidity = errors.New("mdoc: outside validity window")
	// ErrDocTypeMismatch: MSO docType != Document docType, or != ExpectedDocType.
	ErrDocTypeMismatch = errors.New("mdoc: docType mismatch")
	// ErrDeviceMacUnsupported: deviceMac present; remote flows require a
	// signature (WP-03 Decisions). Maps to err:credential:binding-failed.
	ErrDeviceMacUnsupported = errors.New("mdoc: deviceMac not supported (use deviceSignature)")
	// ErrDigestAlg: MSO digestAlgorithm not in the SHA-2 allow-list.
	ErrDigestAlg = errors.New("mdoc: MSO digest algorithm not allowed")
	// ErrStatus: MSO status extension present but malformed.
	ErrStatus = errors.New("mdoc: malformed status extension")
	// ErrDeviceResponseStatus: DeviceResponse.status is non-zero, or
	// documentErrors is non-empty (ISO 18013-5 §8.3.2.1.2.3 Table: 0 = OK).
	// The wallet itself signaled a problem producing the response; treated as
	// untrustworthy as a whole rather than partially trusted (hard rule 7).
	ErrDeviceResponseStatus = errors.New("mdoc: DeviceResponse reports a non-OK status")
)

Sentinel errors. All are wrapped with %w and carry only safe context (namespaces, element identifiers, digest IDs, doctypes) — never element values (hard rule 3). Services map these to conventions.md reason codes.

Functions

func DevicePresent

func DevicePresent(ctx context.Context, deviceKey crypto.Signer, issuerSigned []byte, disclose map[string][]string, st SessionTranscript) ([]byte, error)

DevicePresent produces a DeviceResponse from an issued IssuerSigned by disclosing only the requested elements and signing the DeviceAuthentication with the holder's device key, bound to st (ISO 18013-5 §9.1.3). This is the test-wallet counterpart to Verify.

func KnownDocType

func KnownDocType(docType string) bool

KnownDocType reports whether docType is in the registry (ARF 2.9 PID / ISO 18013-5 mDL). Unknown doctypes still verify; they are annotated, not rejected.

Types

type DeviceAuth

type DeviceAuth struct {
	DeviceSignature cbor.RawMessage `cbor:"deviceSignature,omitempty"`
	DeviceMac       cbor.RawMessage `cbor:"deviceMac,omitempty"`
}

DeviceAuth — ISO 18013-5 §9.1.3.4. Exactly one of deviceSignature (supported) or deviceMac (unsupported for remote flows, WP-03 Decisions) is present.

type DeviceKeyInfo

type DeviceKeyInfo struct {
	DeviceKey         cbor.RawMessage         `cbor:"deviceKey"`
	KeyAuthorizations cbor.RawMessage         `cbor:"keyAuthorizations,omitempty"`
	KeyInfo           map[int]cbor.RawMessage `cbor:"keyInfo,omitempty"`
}

DeviceKeyInfo — ISO 18013-5 §9.1.2.4. deviceKey is a COSE_Key, kept raw and parsed by parseCOSEKey (T-03.3); curve is validated via ECCG policy.

type DeviceResponse

type DeviceResponse struct {
	Version        string          `cbor:"version"`
	Documents      []Document      `cbor:"documents,omitempty"`
	DocumentErrors []DocumentError `cbor:"documentErrors,omitempty"`
	Status         uint            `cbor:"status"`
}

DeviceResponse — ISO 18013-5 §8.3.2.1.2.2. Text-keyed CBOR map.

func DecodeDeviceResponse

func DecodeDeviceResponse(raw []byte) (*DeviceResponse, error)

DecodeDeviceResponse parses an ISO 18013-5 DeviceResponse from untrusted CBOR using the hardened decoder. Tolerant of unknown map members (CDDL forward-compatibility) but strict on the documented shape. Never panics on malformed input (hard rule 5; fuzzed by FuzzDecodeDeviceResponse).

A non-zero top-level status or a non-empty documentErrors is the wallet's own signal that something went wrong producing the response (ISO 18013-5 §8.3.2.1.2.3 Table: 0 = OK); go-mdoc rejects the whole response rather than verifying whatever documents did decode (hard rule 7 — no partial trust of a response the wallet itself flagged as broken).

ISO 18013-5 §8.3.2.1.2.2 DeviceResponse.

type DeviceSigned

type DeviceSigned struct {
	NameSpaces cbor.RawMessage `cbor:"nameSpaces"`
	DeviceAuth DeviceAuth      `cbor:"deviceAuth"`
}

DeviceSigned — ISO 18013-5 §8.3.2.1.2.2. nameSpaces is DeviceNameSpacesBytes (#6.24(bstr .cbor DeviceNameSpaces)); kept raw for the DeviceAuthentication.

type DigestIDs

type DigestIDs map[uint][]byte

DigestIDs — DigestID(uint) => Digest(bstr).

type Document

type Document struct {
	DocType      string       `cbor:"docType"`
	IssuerSigned IssuerSigned `cbor:"issuerSigned"`
	DeviceSigned DeviceSigned `cbor:"deviceSigned"`
	Errors       Errors       `cbor:"errors,omitempty"`
}

Document — ISO 18013-5 §8.3.2.1.2.2.

type DocumentError

type DocumentError map[string]int

DocumentError — ISO 18013-5 §8.3.2.1.2.2: DocType => ErrorCode(int).

type DocumentReport

type DocumentReport struct {
	DocType      string
	KnownDocType bool
	Elements     []ElementReport // sorted by namespace, then identifier
}

DocumentReport annotates a VerifiedDocument for the verification report.

func Report

Report annotates the disclosed namespaces of a verified document using the doctype/namespace registry and its value-type hooks. Pure and value-free: Verify (T-03.3–5) never consults this registry, so an unknown doctype still verifies cryptographically — Report is descriptive metadata only, consumed by verifier-core (WP-09) for its client report + redaction hints.

type DocumentTemplate

type DocumentTemplate struct {
	DocType      string
	Namespaces   map[string]map[string]any
	ValidityInfo ValidityInfo
	DigestAlg    string // "SHA-256" (default) | "SHA-384" | "SHA-512"
	Status       *StatusRef
}

DocumentTemplate describes an mdoc to issue. Minimal by design (test wallet + future issuer); the verifier never uses it.

type ElementReport

type ElementReport struct {
	Namespace  string
	Identifier string
	Known      bool // element registered for this doctype's namespace
	PII        bool // portrait/biometric — redact in logs/report (conventions.md)
	TypeValid  bool // value passed the registered type hook (true if no hook)
}

ElementReport annotates one disclosed element for the verification report. It carries identifiers and flags only — never a value (hard rule 3).

type Errors

type Errors map[string]map[string]int

Errors — ISO 18013-5 §8.3.2.1.2.2: NameSpace => (DataElementIdentifier => ErrorCode).

type Issuer

type Issuer struct {
	// contains filtered or unexported fields
}

Issuer signs MobileSecurityObjects with a document-signer key via go-eudi-crypto and stamps the issuer certificate chain (x5chain).

func NewIssuer

func NewIssuer(kp eudicrypto.KeyProvider, keyID string, x5chain [][]byte) *Issuer

NewIssuer returns an Issuer using kp[keyID] as the document-signer key and x5chain (DER) as the issuer chain placed in the IssuerAuth unprotected header.

func (*Issuer) Issue

func (i *Issuer) Issue(ctx context.Context, doc DocumentTemplate, deviceKey crypto.PublicKey) ([]byte, error)

Issue builds an IssuerSigned (ISO 18013-5 §9.1.2): one IssuerSignedItem per element (fresh 32-byte random salt), the ValueDigests over the exact IssuerSignedItemBytes, an MSO sealing the device key, and the COSE_Sign1 IssuerAuth. deviceKey must be an ECCG-allowed EC public key.

type IssuerChainResolver

type IssuerChainResolver func(x5chain [][]byte) (dsKey crypto.PublicKey, err error)

IssuerChainResolver resolves the issuer certificate chain (x5chain, DER) to the document-signer public key. It is the trust boundary: go-mdoc stays trust-agnostic (ADR-0004) and verifier-core wires this to go-eudi-trust.

type IssuerNameSpaces

type IssuerNameSpaces map[string][]cbor.RawMessage

IssuerNameSpaces — NameSpace => [+ IssuerSignedItemBytes]. Each element is a #6.24(bstr .cbor IssuerSignedItem); kept raw so digests hash wire bytes.

type IssuerSigned

type IssuerSigned struct {
	NameSpaces IssuerNameSpaces `cbor:"nameSpaces,omitempty"`
	IssuerAuth cbor.RawMessage  `cbor:"issuerAuth"`
}

IssuerSigned — ISO 18013-5 §9.1.2.4. issuerAuth is a COSE_Sign1 kept raw and delegated to go-eudi-crypto (hard rule 4).

type IssuerSignedItem

type IssuerSignedItem struct {
	DigestID          uint            `cbor:"digestID"`
	Random            []byte          `cbor:"random"`
	ElementIdentifier string          `cbor:"elementIdentifier"`
	ElementValue      cbor.RawMessage `cbor:"elementValue"` // any DataElementValue, kept raw
}

IssuerSignedItem — ISO 18013-5 §9.1.2.4.

type MobileSecurityObject

type MobileSecurityObject struct {
	Version         string          `cbor:"version"`
	DigestAlgorithm string          `cbor:"digestAlgorithm"` // "SHA-256"|"SHA-384"|"SHA-512"
	ValueDigests    ValueDigests    `cbor:"valueDigests"`
	DeviceKeyInfo   DeviceKeyInfo   `cbor:"deviceKeyInfo"`
	DocType         string          `cbor:"docType"`
	ValidityInfo    ValidityInfo    `cbor:"validityInfo"`
	Status          cbor.RawMessage `cbor:"status,omitempty"`
}

MobileSecurityObject — ISO 18013-5 §9.1.2.4. status is the IETF Token Status List extension (draft-ietf-oauth-status-list, T-03.8); kept raw, parsed lazily.

type Option

type Option func(*Verifier)

Option configures a Verifier.

func WithClock

func WithClock(now func() time.Time) Option

WithClock injects the time source for ValidityInfo checks (conventions.md: inject func() time.Time into anything validating validity windows).

type SessionTranscript

type SessionTranscript struct {
	// contains filtered or unexported fields
}

SessionTranscript is the ISO 18013-5 §9.1.5.1 / ISO/IEC TS 18013-7 Annex B SessionTranscript = [DeviceEngagementBytes, EReaderKeyBytes, Handover], carried as its exact CBOR encoding. It is opaque to callers of Verify: built by go-oid4vp via the OID4VPHandover / OID4VPDCAPIHandover constructors (T-03.6) and consumed by device-authentication transcript binding (T-03.5).

func OID4VPDCAPIHandover

func OID4VPDCAPIHandover(origin, nonce, jwkThumbprint string) SessionTranscript

OID4VPDCAPIHandover builds the SessionTranscript for the W3C Digital Credentials API flow: SessionTranscript = [null, null, OID4VPDCAPIHandover], OID4VPDCAPIHandover = ["OpenID4VPDCAPIHandover", SHA-256(CBOR(OpenID4VPDCAPIHandoverInfo))], OpenID4VPDCAPIHandoverInfo = [origin, nonce, jwkThumbprint] (OpenID4VP 1.0 Annex B.2.6.2; no client_id or response_uri in this variant — origin carries the RP identity signal instead). jwkThumbprint: see OID4VPHandover's doc comment (same FLAG applies).

func OID4VPHandover

func OID4VPHandover(clientID, nonce, jwkThumbprint, responseURI string) SessionTranscript

OID4VPHandover builds the SessionTranscript for the redirect (non-DC-API) OpenID4VP flow: SessionTranscript = [null, null, OID4VPHandover], where OID4VPHandover = ["OpenID4VPHandover", SHA-256(CBOR(OpenID4VPHandoverInfo))] and OpenID4VPHandoverInfo = [client_id, nonce, jwkThumbprint, response_uri] (OpenID4VP 1.0 Annex B.2.6.1). jwkThumbprint is the RFC 7638 thumbprint of the RP's ephemeral encryption key when the response is JWE-encrypted (response_mode=direct_post.jwt); pass "" (encoded as CBOR null) when it is not. SHA-256 is fixed by the profile — a spec constant, not an ECCG-negotiable choice, so it is not routed through the algorithm allow-list.

FLAG (2026-07-06 EU cross-check, docs/mdoc-eu-gap-report.md): this replaces an earlier, unverified 3-hash-tuple construction that never matched any known implementation. Corrected to match the EU reference verifier (eudi-srv-verifier-endpoint-main, DocumentValidator.kt buildOpenId4VpHandover) after the OpenID4VP 1.0 Annex B text could not be fetched this session (WebFetch truncates before Annex B; no vendored copy in references/). The outer shape (identifier + single HandoverInfo hash) and inner element order/membership are well-corroborated against that production reference; jwkThumbprint's exact CBOR type (tstr assumed here) is NOT yet confirmed against primary spec text — re-verify before wiring in an encrypted-response (WP-08) deployment.

func (SessionTranscript) Bytes

func (s SessionTranscript) Bytes() []byte

Bytes returns the exact CBOR encoding of the SessionTranscript array. Used for golden-vector byte-exactness (T-03.6) and by go-oid4vp when it needs the on-the-wire transcript.

type StatusRef

type StatusRef struct {
	URI   string
	Index uint
	Raw   map[string]any
}

StatusRef is the MSO status extension reference (IETF Token Status List, draft-ietf-oauth-status-list) surfaced to the caller. go-mdoc only extracts it; fetching and evaluating the status list is WP-04 (go-statuslist). Raw preserves the full status map for forward-compatibility (e.g. identifier_list).

T-03.3 note: only the type existed here originally so VerifiedDocument.Status type-checked against the WP-03 README "Locked public interface" (hard rule 11, binding from T-03.3 onward). T-03.8 adds parseMSOStatus below and wires VerifiedDocument.Status in verify.go.

type ValidityInfo

type ValidityInfo struct {
	Signed         time.Time  `cbor:"signed"`
	ValidFrom      time.Time  `cbor:"validFrom"`
	ValidUntil     time.Time  `cbor:"validUntil"`
	ExpectedUpdate *time.Time `cbor:"expectedUpdate,omitempty"`
}

ValidityInfo — ISO 18013-5 §9.1.2.4. tdate (#6.0 tstr) fields; also the public output type embedded in VerifiedDocument (README target).

type ValueDigests

type ValueDigests map[string]DigestIDs

ValueDigests — NameSpace => DigestIDs.

type VerifiedDocument

type VerifiedDocument struct {
	DocType      string
	Namespaces   map[string]map[string]any
	ValidityInfo ValidityInfo
	DeviceKey    crypto.PublicKey
	Status       *StatusRef
	MSODigestAlg string
}

VerifiedDocument is the result for one Document: disclosed + digest-checked namespaces, the validity window, the device key, an optional status ref, and the MSO digest algorithm. Namespaces carry disclosed values forwarded to the caller (ADR-0005) and are never persisted.

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier holds the injected clock and hardened codec (package-level decMode).

func NewVerifier

func NewVerifier(opts ...Option) *Verifier

NewVerifier returns a Verifier defaulting to time.Now.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, in VerifyInput) ([]VerifiedDocument, error)

Verify parses and verifies a DeviceResponse for remote flows. Fail closed: any failing check aborts with a precise sentinel (hard rule 7). verifyDocument performs MSO (issuer) authentication, issuer-data integrity, device authentication, and status-reference extraction, in that order.

ISO 18013-5 §8.3 / §9.1; ISO/IEC TS 18013-7 Annex B.

type VerifyInput

type VerifyInput struct {
	DeviceResponse      []byte
	SessionTranscript   SessionTranscript
	IssuerChainResolver IssuerChainResolver
	ExpectedDocType     string
}

VerifyInput bundles one DeviceResponse with the session binding and the trust callback. SessionTranscript is opaque here (built by go-oid4vp via the constructors in T-03.6) and is enforced by device-auth transcript binding (verifyDeviceAuth): an empty/mismatched transcript fails closed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL