Material Profiles
A material profile defines the recipe for constructing an identity link binding during reconciliation. It specifies which identifiers to hash and store (the "materials"), which attributes to persist in the encrypted envelope, and how to merge attributes from different sources. Think of it as a template that controls what data gets captured during the one-time reconciliation event.
Material profiles are referenced by selector rules. When a rule's plan is RUN_IDV or STEP_UP, it includes a material-profile-id that points to the profile to use. This indirection means you can change what data gets captured without changing the rules themselves, and you can share a single profile across multiple rules.
Material Types
Four types of materials can be specified in a profile. Each material type produces a different kind of identity_match record in the database, representing a different lookup path to the same identity_link_binding.
HolderKeyMaterial
Hashes the wallet holder's public key fingerprint using Key A (the holder HMAC key). The fingerprint is computed as the JWK thumbprint (RFC 7638) of the holder key extracted from the Verifiable Presentation's proof. The resulting HMAC hash is stored as a KEY-type identity_match record.
This is the primary lookup path for wallet authentication. When a user presents their wallet, the system computes the JWK thumbprint of the holder key, HMACs it with Key A, and searches for a matching identity_match record. If found, the associated identity_link_binding is retrieved and used.
The JWK thumbprint is a deterministic, canonical representation of the public key that is independent of key serialization format. This means the same physical key always produces the same thumbprint regardless of how it is encoded in the VP.
ProviderSubjectMaterial
Hashes the OIDC subject identifier (the sub claim or another configured claim) from the reconciliation provider's response using Key B (the institution HMAC key). The resulting hash is stored as a SUBJECT_ID-type identity_match record.
This material enables reverse lookup: given an institutional identifier, find the associated wallet binding. The external REST API uses this path to look up identities by institutional ID hash. Without this material, lookups can only go in one direction (wallet key to identity). With it, lookups are bidirectional (wallet key to identity, and institutional ID to identity).
The choice of which claim to hash is configured on the reconciliation provider via the identifier-attribute-name field. For SURFconext, this is typically the sub claim, which is a persistent, pairwise pseudonymous identifier unique to each user-client combination.
AttributeTupleMaterial
Creates a composite hash from multiple claim values extracted from the OIDC provider's response. For example, a hash of given_name + family_name + date_of_birth could be used as a composite lookup key. The resulting hash is stored as a CLAIM_TUPLE-type identity_match record.
This material is useful for matching when no unique single identifier exists, or as a fallback when the primary identifier changes. For example, if a user's institutional sub claim changes (perhaps due to a migration), the attribute tuple can still match them to their existing binding.
The composite hash is order-dependent: the claim values are concatenated in the order specified in the profile, separated by a delimiter, and then HMAC-hashed. Changing the order or adding/removing claims produces a different hash, so the tuple definition should be treated as immutable once bindings have been created with it.
CredentialAttributeTupleMaterial
Similar to AttributeTupleMaterial but uses claims extracted from the wallet credential itself rather than from the OIDC provider's response. This is useful when the wallet credential contains identifying attributes that should serve as an additional lookup path.
For example, if the wallet credential includes a student number or national identifier, a CredentialAttributeTupleMaterial can hash that value to create a lookup path from the credential attribute to the binding. This enables scenarios where the wallet key has changed (e.g., the user reinstalled their wallet app) but the credential still contains the same identifying attributes.
Portal's Default Profile
The portal ships with a minimal material profile that creates a single lookup path:
material-profiles:
- id: "holder-only-v1"
version: "1"
materials:
- type: "holder_key_fp"
hmac-domain: "holder"
This profile creates a single identity_match record of type KEY, derived from the wallet holder key fingerprint hashed with the "holder" HMAC domain (Key A).
What this means in practice:
- The system can look up an identity from a wallet key. When a user presents their wallet, the holder key fingerprint is hashed and used to find the binding. This is the primary authentication path.
- The system cannot look up an identity from an institutional ID alone. There is no
ProviderSubjectMaterial, so the reverse lookup path does not exist. The external REST API's "lookup by institutional ID" endpoint will not find any matches. - There is no fallback matching. If the user's wallet key changes (e.g., they reinstall their wallet app), the system will treat them as a new user and require fresh reconciliation.
This is sufficient for the PoC, where all lookups originate from wallet authentication and there are no external API consumers that need reverse lookup. For production deployments, a more complete profile is recommended.
Extended Profile Example
A production-ready profile might include multiple materials for bidirectional lookup:
material-profiles:
- id: "holder-plus-institution-v1"
version: "1"
materials:
- type: "holder_key_fp"
hmac-domain: "holder"
- type: "provider_subject"
hmac-domain: "institution"
claim-name: "sub"
This profile creates two identity_match records for each binding:
- KEY type -- from the holder key fingerprint, hashed with Key A ("holder" domain). Used for wallet-initiated lookups during authentication.
- SUBJECT_ID type -- from the OIDC provider's
subclaim, hashed with Key B ("institution" domain). Used for institution-initiated lookups via the external REST API.
With both materials in place, the identity graph supports bidirectional traversal:
- Wallet to identity: User presents wallet credential, system hashes holder key with Key A, finds KEY-type match, retrieves binding.
- Institution to identity: External system provides institutional ID, system hashes it with Key B, finds SUBJECT_ID-type match, retrieves binding.
The two HMAC domains (Key A and Key B) are deliberately separate. Even if one key is compromised, the attacker cannot use it to forge lookups in the other direction. This key separation is a core privacy design principle.
Attribute Rules
Attribute rules control what gets persisted in the encrypted binding envelope and what gets projected into the STS token. They are defined within the material profile and apply during the reconciliation ceremony when claims are collected from both the wallet credential and the OIDC provider.
attribute-rules:
- canonical-name: "eduid"
merge-mode: "OIDC_ONLY"
persist: true
project: true
source-aliases:
- "eduid"
- canonical-name: "eduperson_principal_name"
merge-mode: "OIDC_WINS"
persist: true
project: true
source-aliases:
- "eduperson_principal_name"
- "urn:mace:dir:attribute-def:eduPersonPrincipalName"
- canonical-name: "given_name"
merge-mode: "OIDC_WINS"
persist: false
project: true
Each attribute rule has the following fields:
merge-mode
Determines how to resolve conflicts when both the wallet credential and the OIDC provider supply a value for the same canonical attribute.
OIDC_WINS-- Use the OIDC provider's value if available. If the OIDC provider does not supply the attribute, fall back to the wallet credential's value. This is the most common mode, as institutional identity providers are generally considered the authoritative source for identity attributes.WALLET_ONLY-- Only use the value from the wallet credential. Ignore the OIDC provider's value entirely, even if available. Use this for attributes that are specifically attested by the credential issuer and should not be overridden by the identity provider.OIDC_ONLY-- Only use the value from the OIDC provider. Never source this attribute from the wallet credential. Use this for institution-specific identifiers (likeeduid) that only the identity provider can authoritatively supply.
persist
Whether to include this attribute in the encrypted binding envelope that is written to the database. Persisted attributes are available on subsequent authentications without requiring a fresh OIDC call. Set to true for attributes that are needed on the fast path (UseExistingBinding) and false for attributes that are only needed during the initial reconciliation.
Consider data minimization when deciding what to persist. Every persisted attribute increases the sensitivity of the encrypted envelope. Only persist attributes that are genuinely needed for subsequent authentications.
project
Whether to include this attribute in the STS token claims that are made available to downstream systems (relying parties). An attribute can be projected without being persisted (it is included in the token during the reconciliation flow but not stored for future use) or persisted without being projected (stored for internal use but not exposed to relying parties).
source-aliases
A list of alternative claim names from different providers that map to this canonical attribute name. During reconciliation, the system collects claims from both the wallet credential and the OIDC provider. These claims may use different names for the same semantic attribute. Source aliases handle this normalization.
Canonical Attribute Normalization
Different identity providers and credential issuers use different claim names for the same data. This is a practical reality of federated identity: OIDC providers use one naming convention, SAML providers use URN-based names, and wallet credentials may use yet another vocabulary.
Source aliases handle this mapping transparently. For example, the canonical attribute eduperson_principal_name might arrive as:
eduperson_principal_namefrom a modern OIDC providerurn:mace:dir:attribute-def:eduPersonPrincipalNamefrom a SAML-to-OIDC bridgeeppnfrom a simplified claim mapping
By listing all known aliases in the source-aliases array, the reconciliation engine can recognize the attribute regardless of which name the provider uses. The canonical name is what gets persisted and projected -- the aliases are only used during claim collection.
This normalization happens once, during reconciliation. After that, the binding envelope contains only canonical names, and all downstream systems see a consistent attribute vocabulary.
Version Tracking
Every material profile has a version field, and every identity_link_binding record stores the material_profile_version that was used when the binding was created. This enables detecting when a profile has changed since a binding was last created or refreshed.
When the reconciliation engine encounters an existing binding whose material_profile_version does not match the current profile's version, it knows the profile has been updated. Depending on the deployment's policy, the system can:
- Ignore the mismatch and continue using the existing binding as-is. The binding still works, but it may be missing materials or attributes that were added in the newer profile version.
- Trigger re-materialization on the next authentication. The user goes through a fresh reconciliation flow, and the binding is recreated with the new profile's materials and attribute rules. This ensures the binding is up-to-date but requires user interaction.
- Flag the binding for batch migration. An administrative process can identify all bindings with outdated profile versions and schedule them for re-materialization.
The version field is an opaque string -- there is no automatic comparison or ordering. It is the operator's responsibility to set a new version when the profile changes meaningfully. A common convention is to use a date string (e.g., "2026-03-24") or a sequential number.