Skip to main content

Cryptographic Keys

This page provides detailed specifications for the three domain-separated cryptographic keys used by the portal: Key A (holder HMAC), Key B (institution HMAC), and Key C (data encryption). Each key has a distinct alias, algorithm, and purpose. Understanding how each key is used is essential for security auditing, key rotation planning, and incident response.

Key A: reconciliation:holder (HMAC-SHA256)

Purpose: Hashes wallet holder public key fingerprints for identity matching lookup.

Key A is used exclusively in the wallet verification flow. When a holder presents a Verifiable Presentation via OID4VP, the portal needs a way to recognize that holder in future interactions without storing the raw public key. Key A enables this by producing a deterministic but irreversible hash of the holder's key fingerprint.

How It Works

The hashing process follows these steps:

  1. Public key extraction: The holder's public key is extracted from the Verifiable Presentation (VP) submitted via the OID4VP protocol. This key is typically an EC key on the P-256 curve.

  2. JWK thumbprint computation: The JWK thumbprint is computed according to RFC 7638. This produces a deterministic fingerprint of the key by serializing the key's essential parameters in a canonical JSON form and hashing the result with SHA-256. The thumbprint is the same regardless of how the key is represented (PEM, JWK, etc.).

  3. HMAC computation: HMAC-SHA256(jwk_thumbprint, Key_A) is computed. The HMAC function takes the JWK thumbprint as the message and Key A as the secret, producing a 256-bit (32-byte) output.

  4. Encoding: The HMAC output is encoded as a multibase-encoded multihash string. This self-describing format includes the hash algorithm identifier and the hash length, making it forward-compatible with future algorithm changes.

  5. Storage: The encoded hash is stored as identifier_hash in the identity_match table (with type=KEY) and as holder_identifier_hash in the identity_link_binding table.

Properties

  • Deterministic: The same holder public key always produces the same hash, enabling reliable lookup across sessions and time.
  • Irreversible: It is computationally infeasible to recover the original public key or JWK thumbprint from the hash. HMAC-SHA256 is a one-way function.
  • Collision-resistant: It is practically impossible for two different public keys to produce the same hash. The probability of a collision is approximately 1 in 2^128.
  • Key-dependent: Without Key A, the hash cannot be computed or verified. An attacker with database access but no key access cannot determine which hash corresponds to which public key, even if they know the public keys.

Configuration

identity.reconciliation.crypto:
holder-hmac-key-alias: "reconciliation:holder"
holder-hmac-key-version: 1

The holder-hmac-key-alias specifies the key alias in the KMS provider. The holder-hmac-key-version tracks the current active version of the key, which is important for key rotation (see Key Version Management below).


Key B: reconciliation:institution (HMAC-SHA256)

Purpose: Hashes institutional identifiers (SURFconext subject IDs, eduperson_principal_name) for identity matching lookup.

Key B serves a parallel role to Key A but operates on a completely different class of identifiers. While Key A hashes wallet-side cryptographic key fingerprints, Key B hashes institution-side user identifiers. These are the identifiers that SURFconext (or another OIDC provider) returns when a user authenticates with their institutional account.

How It Works

  1. Identifier retrieval: During the reconciliation flow, the user authenticates with their institutional identity provider via SURFconext. The OIDC provider returns the user's institutional identifier, typically the sub claim (a persistent, pairwise pseudonymous identifier) or the eduperson_principal_name.

  2. HMAC computation: HMAC-SHA256(institutional_id, Key_B) is computed. Key B is used as the HMAC secret, and the institutional identifier is the message.

  3. Encoding and storage: The result is encoded as a multibase-encoded multihash string and stored as identifier_hash in the identity_match table (with type=SUBJECT_ID) and as institution_identifier_hash in the identity_link_binding table.

Why a Separate Key from Key A?

It may seem simpler to use a single HMAC key for both holder keys and institutional identifiers, but domain separation provides critical security benefits:

  • Cross-correlation prevention: Even with full database access and one compromised key, an attacker cannot determine which wallet belongs to which institutional account. Compromising Key A reveals which database rows correspond to known public keys, but without Key B, those rows cannot be linked to institutional identifiers. The reverse is also true.

  • Independent compromise containment: If Key A is compromised through a vulnerability in the wallet verification flow, Key B remains secure. The attacker's ability to compute holder key hashes does not extend to institutional identifier hashes.

  • Regulatory compliance: Different data categories (wallet identifiers vs. institutional identifiers) may be subject to different regulatory requirements regarding key lifecycle, rotation frequency, and access control. Separate keys enable separate policies.

  • Incident response clarity: In the event of a key compromise, the blast radius is immediately clear. Security teams know exactly which data category is affected and can scope their response accordingly.

Configuration

identity.reconciliation.crypto:
institution-hmac-key-alias: "reconciliation:institution"
institution-hmac-key-version: 1

Key C: reconciliation:encryption (AES-256-GCM)

Purpose: Encrypts all sensitive data that must be recoverable -- the institution-scoped eduID, canonical claims, auxiliary data, and reconciliation session data.

While Keys A and B produce irreversible hashes suitable for lookup, Key C provides reversible encryption for data that the portal must be able to read back. This includes the user's actual institutional identifier (needed for token issuance), cached user attributes (needed for fast-path token projection), and institution-specific metadata.

What Gets Encrypted with Key C

DataStorage LocationWhy It Is Encrypted
Institution-scoped eduIDidentity_link_binding.encrypted_institution_idThe actual institutional identifier must be recoverable for STS token issuance, but must not be stored in plaintext
Canonical claimsidentity_link_binding.persisted_attributes_envelopeCached user attributes (name, email, etc.) for fast-path token projection without re-authenticating
Resolved identityreconciliation_session.encrypted_identityTemporary storage of claims during the reconciliation flow; short-lived but still protected
Auxiliary dataauxiliary_data.encrypted_payloadInstitution-specific metadata such as enrollment status, role, or programme information

How AES-256-GCM Works

AES-256-GCM (Advanced Encryption Standard with 256-bit key in Galois/Counter Mode) is an authenticated encryption algorithm. It provides both confidentiality (data cannot be read without the key) and integrity (any tampering with the ciphertext is detected on decryption). Here is how it operates in the portal:

  • 256-bit key: Provides a 128-bit security level, which is considered secure against both classical and near-term quantum attacks. The key is 32 bytes long and is generated by the KMS provider.

  • Unique IV per operation: Each encryption operation generates a fresh random 12-byte initialization vector (IV), also called a nonce. This ensures that encrypting the same plaintext twice produces different ciphertext, preventing pattern analysis.

  • Galois/Counter Mode: GCM combines counter-mode encryption (for confidentiality) with Galois-field multiplication (for authentication). The counter mode turns AES into a stream cipher, and the Galois-field computation produces an authentication tag.

  • Authentication tag: Each encryption produces a 16-byte authentication tag that is stored alongside the ciphertext. On decryption, the tag is verified first. If the ciphertext has been modified in any way (even a single bit flip), the tag verification fails and decryption is rejected. This prevents both accidental corruption and deliberate tampering.

  • Output format: The encrypted output is structured as IV (12 bytes) + ciphertext + authentication tag (16 bytes). This is encoded and stored as a TEXT column in the database.

Configuration

identity.reconciliation.crypto:
encryption-key-alias: "reconciliation:encryption"
encryption-key-version: 1

Key Version Management

Every record in the database stores the key version that was used when it was created or last updated. This is essential for supporting key rotation without requiring a full database re-encryption in a single operation.

The version is tracked in the following columns:

identity_match.hash_key_version              → Key A or Key B version (depending on match type)
identity_link_binding.holder_hash_key_version → Key A version
identity_link_binding.institution_hash_key_version → Key B version
identity_link_binding.encrypted_institution_id_key_version → Key C version
auxiliary_data (implicitly via schema_version) → Key C version

What Version Tracking Enables

  • Targeted migration during key rotation: When a key is rotated, only records created with the old version need to be re-processed. The system can query for records with a specific key_version and re-hash or re-encrypt them with the new key version. This avoids a costly full-table migration.

  • Audit trail: For any given record, you can determine exactly which key version was used to create it. This is valuable for security audits, incident investigations, and compliance reporting.

  • Dual-read during rotation: During a key rotation window, the system can attempt decryption or hash verification with the current key version first, and if that fails, fall back to the previous version. This enables zero-downtime key rotation where old and new versions coexist temporarily.

  • Forward compatibility: If the cryptographic algorithm itself needs to change in the future (for example, migrating from HMAC-SHA256 to a post-quantum hash), the version tracking infrastructure is already in place to support a gradual migration.


AuthBridgeKeyInitializer

On application startup, the AuthBridgeKeyInitializer ensures that all required cryptographic keys exist in the configured KMS provider. If a key does not yet exist (typical for first-time setup with the software KMS provider), it is generated automatically.

// Simplified from AuthBridgeKeyInitializer.kt
object AuthBridgeKeyInitializer {
suspend fun initialize(kms: KeyManagerService, config: AuthBridgeConfig) {
// JAR Signing Key (ES256 for OID4VP request objects)
kms.getOrGenerateKey(
alias = config.jarSigningKeyAlias, // "auth-bridge-jar-signing-key"
algorithm = Algorithm.ES256,
keyType = KeyType.EC_P256
)

// Key A: Holder HMAC
kms.getOrGenerateKey(
alias = config.holderHmacKeyAlias, // "reconciliation:holder"
algorithm = Algorithm.HS256,
keyLength = 256
)

// Key B: Institution HMAC
kms.getOrGenerateKey(
alias = config.institutionHmacKeyAlias, // "reconciliation:institution"
algorithm = Algorithm.HS256,
keyLength = 256
)

// Key C: Encryption
kms.getOrGenerateKey(
alias = config.encryptionKeyAlias, // "reconciliation:encryption"
algorithm = Algorithm.A256GCM,
keyLength = 256
)
}
}

The getOrGenerateKey method is idempotent: if the key already exists under the given alias, it returns the existing key without modification. If the key does not exist, it generates a new one with the specified algorithm and key length.

note

In production deployments using Azure Key Vault, keys are pre-provisioned by infrastructure automation (such as Terraform or ARM templates) and marked as non-exportable. The initializer in this case only verifies that the expected keys exist and are accessible. It does not attempt to create new keys, as the KMS provider's access policy would not permit key creation from the application.

The JAR signing key (ES256/P-256) is listed here for completeness, but it is not part of the three-key encryption model. It is an asymmetric key used for signing OID4VP request objects, and its public key is published as part of the verifier's did:jwk identity. See the Authentication Flows documentation for details on how the JAR signing key is used in OID4VP request objects.