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: it does no logging, takes a context.Context and an IssuerTrust boundary for trust, and delegates every COSE/X.509 operation to go-eudi-crypto (ECCG-pinned policy). It never names a COSE/JWS algorithm; the sole MSO digest-algorithm allow-list (digest.go) is flagged for migration into go-eudi-crypto.
Trust-agnostic does not mean time-agnostic: the package asserts that a credential's signing time lies inside its document signer certificate's validity window, and hands that time to IssuerTrust so the implementation can decide which instant the certificate path is judged at.
deviceMac (session-encryption MAC) is out of scope for remote presentment.
Index ¶
- Variables
- func DevicePresent(ctx context.Context, deviceKey crypto.Signer, issuerSigned []byte, ...) ([]byte, error)
- func KnownDocType(docType string) bool
- type DeviceAuth
- type DeviceKeyInfo
- type DeviceResponse
- type DeviceSigned
- type DigestIDs
- type Document
- type DocumentError
- type DocumentReport
- type DocumentTemplate
- type ElementReport
- type Errors
- type Issuer
- type IssuerNameSpaces
- type IssuerSigned
- type IssuerSignedItem
- type IssuerTrust
- type MobileSecurityObject
- type Option
- type SessionTranscript
- type StatusRef
- type ValidityInfo
- type ValueDigests
- type VerifiedDocument
- type Verifier
- type VerifyInput
Constants ¶
This section is empty.
Variables ¶
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") // ErrIssuerCertValidity: the signing time the credential carries lies // outside the validity window of the document signer certificate in the MSO // header ([ISO/IEC 18013-5 §9.3.1] step 5). Kept apart from ErrIssuerAuth so // a certificate-window problem is never reported as an unknown issuer — the // remedies are different and one of them wastes an operator's day. Maps to // err:credential:issuer-cert-expired. ErrIssuerCertValidity = errors.New("mdoc: signing time outside the document signer certificate validity window") // 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 (remote flows use a signature). 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/IEC 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. 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. Services map these to 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/IEC 18013-5 §9.1.3]). This is the test-wallet counterpart to Verify.
func KnownDocType ¶
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/IEC 18013-5 §9.1.3.4]. Exactly one of deviceSignature (supported) or deviceMac (unsupported for remote flows) 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/IEC 18013-5 §9.1.2.4]. deviceKey is a COSE_Key, kept raw and parsed by parseCOSEKey; 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/IEC 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 (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/IEC 18013-5 §8.3.2.1.2.3] Table: 0 = OK); go-mdoc rejects the whole response rather than verifying whatever documents did decode (no partial trust of a response the wallet itself flagged as broken).
[ISO/IEC 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/IEC 18013-5 §8.3.2.1.2.2]. nameSpaces is DeviceNameSpacesBytes (#6.24(bstr .cbor DeviceNameSpaces)); kept raw for the DeviceAuthentication.
type Document ¶
type Document struct {
DocType string `cbor:"docType"`
IssuerSigned IssuerSigned `cbor:"issuerSigned"`
DeviceSigned DeviceSigned `cbor:"deviceSigned"`
Errors Errors `cbor:"errors,omitempty"`
}
Document — [ISO/IEC 18013-5 §8.3.2.1.2.2].
type DocumentError ¶
DocumentError — [ISO/IEC 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 ¶
func Report(vd VerifiedDocument) DocumentReport
Report annotates the disclosed namespaces of a verified document using the doctype/namespace registry and its value-type hooks. Pure and value-free: Verify never consults this registry, so an unknown doctype still verifies cryptographically — Report is descriptive metadata only, consumed by eudi-verifier-core 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
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.
type Errors ¶
Errors — [ISO/IEC 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/IEC 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 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/IEC 18013-5 §9.1.2.4]. issuerAuth is a COSE_Sign1 kept raw and delegated to go-eudi-crypto.
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/IEC 18013-5 §9.1.2.4].
type IssuerTrust ¶ added in v0.1.0
type IssuerTrust interface {
ResolveIssuerKey(x5chain [][]byte, signed time.Time) (dsKey crypto.PublicKey, err error)
}
IssuerTrust is the trust boundary: go-mdoc stays trust-agnostic and the verifier wires this to its trust layer.
ResolveIssuerKey receives the issuer certificate chain (x5chain, DER, leaf first) and the signing time the credential claims, and returns the document signer's public key. The signing time is passed because a document signer is short-lived while the credentials it signed stay in wallets far longer, so the implementation — not this library — decides which instant the certificate path is judged at. Both models are legitimate and the choice belongs to the relying party's risk appetite.
The claimed time is NOT authenticated when this is called: it is read from the issuer-signed structure whose signature is verified with the key this call returns. It selects a validation time and authorizes nothing. Verify independently asserts that it falls inside the signer certificate's own validity window, and re-asserts after the signature verifies that the authenticated signing time is the value that was passed here.
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/IEC 18013-5 §9.1.2.4]. status is the IETF Token Status List extension (draft-ietf-oauth-status-list); kept raw, parsed lazily.
type SessionTranscript ¶
type SessionTranscript struct {
// contains filtered or unexported fields
}
SessionTranscript is the [ISO/IEC 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 and consumed by device-authentication transcript binding.
func OID4VPDCAPIHandover ¶
func OID4VPDCAPIHandover(origin, nonce string, jwkThumbprint []byte) 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 clientId or responseUri in this variant — origin carries the RP identity signal instead, and must not carry an "origin:" prefix).
jwkThumbprint: raw digest bytes or nil, exactly as for OID4VPHandover. It is present for response mode dc_api.jwt and null for plain dc_api.
func OID4VPHandover ¶
func OID4VPHandover(clientID, nonce string, jwkThumbprint []byte, 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 = [clientId, nonce, jwkThumbprint, responseUri] ([OpenID4VP 1.0 Annex B.2.6.1]). 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.
clientID is the client_id request parameter INCLUDING its Client Identifier Prefix (e.g. "x509_san_dns:verifier.example.com"); responseURI is whichever of response_uri / redirect_uri the response mode used.
jwkThumbprint is the RFC 7638 JWK SHA-256 thumbprint of the RP's ephemeral response-encryption public key as RAW DIGEST BYTES, encoded on the wire as a CBOR byte string — never the printable base64url form, which hashes to a different transcript and would fail device authentication against every conformant wallet. Pass nil when the response is not encrypted; that encodes as CBOR null, as the spec requires.
func (SessionTranscript) Bytes ¶
func (s SessionTranscript) Bytes() []byte
Bytes returns the exact CBOR encoding of the SessionTranscript array. Used for golden-vector byte-exactness and by go-oid4vp when it needs the on-the-wire transcript.
type StatusRef ¶
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 go-statuslist. Raw preserves the full status map for forward-compatibility (e.g. identifier_list).
Note: only the type existed here originally so VerifiedDocument.Status type-checked against the locked public interface; parseMSOStatus below now populates it and verify.go wires VerifiedDocument.Status.
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/IEC 18013-5 §9.1.2.4]. tdate (#6.0 tstr) fields; also the public output type embedded in VerifiedDocument (README target).
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 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 ¶
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 (fail closed). verifyDocument performs MSO (issuer) authentication, issuer-data integrity, device authentication, and status-reference extraction, in that order.
[ISO/IEC 18013-5 §8.3 / §9.1]; ISO/IEC TS 18013-7 Annex B.
type VerifyInput ¶
type VerifyInput struct {
DeviceResponse []byte
SessionTranscript SessionTranscript
IssuerTrust IssuerTrust
ExpectedDocType string
}
VerifyInput bundles one DeviceResponse with the session binding and the trust boundary. SessionTranscript is opaque here (built by go-oid4vp via the constructors) and is enforced by device-auth transcript binding (verifyDeviceAuth): an empty/mismatched transcript fails closed.