Skip to main content
Version: v0.25.0 (Latest)

Key Management

The IDK provides a unified key management system through the KeyManagerService. This service abstracts the details of different key storage backends, allowing you to generate, store, and use cryptographic keys consistently across platforms and providers.

Key management in the IDK covers the full lifecycle of cryptographic keys: generating new key pairs, persisting them in a key store, retrieving them by alias or key ID, and using them for signing, encryption, and key agreement. Because multiple KMS backends can be registered at once (software, hardware, cloud), the KeyManagerService acts as a router that delegates each operation to the correct provider.

KeyManagerService Overview

The KeyManagerService is the central entry point for all cryptographic key operations. It delegates actual key operations to registered KMS providers while presenting a consistent interface.

Key capabilities include:

  • Key generation with configurable algorithms
  • Key storage and retrieval
  • Signing and verification operations
  • Encryption and decryption
  • Key wrapping and unwrapping
  • Key agreement for establishing shared secrets
  • Provider capability querying

Accessing the Service

The KeyManagerService is available from the session graph:

val keyManager = session.graph.keyManagerService

Generating Keys

When generating a key, you choose the signature algorithm (which determines the curve or key size) and optionally target a specific KMS provider. If you pass null for providerId, the service picks the default provider. The alias is a human-readable identifier you can later use to look up the key.

Generate cryptographic key pairs using the generateKeyAsync method:

import com.sphereon.crypto.core.SignatureAlgorithm
import com.sphereon.crypto.jose.JwkUse
import com.sphereon.crypto.core.KeyOperations

// Generate an ECDSA P-256 signing key
val signingKeyPair = keyManager.generateKeyAsync(
providerId = null, // Use default provider
alias = "my-signing-key",
use = JwkUse.sig,
keyOperations = arrayOf(KeyOperations.SIGN, KeyOperations.VERIFY),
alg = SignatureAlgorithm.ECDSA_SHA256,
keyVisibility = KeyVisibility.PRIVATE
)

// Generate with a specific provider
val awsKeyPair = keyManager.generateKeyAsync(
providerId = "aws",
alias = "my-aws-key",
use = JwkUse.sig,
alg = SignatureAlgorithm.ECDSA_SHA256
)

Supported Algorithms

The IDK supports the following signature algorithms:

AlgorithmDescriptionCurve/Size
ECDSA_SHA256ECDSA with SHA-256P-256
ECDSA_SHA384ECDSA with SHA-384P-384
ECDSA_SHA512ECDSA with SHA-512P-521
RSA_SHA256RSA PKCS#1 v1.5 with SHA-2562048+ bits
RSA_SHA384RSA PKCS#1 v1.5 with SHA-3842048+ bits
RSA_SHA512RSA PKCS#1 v1.5 with SHA-5122048+ bits
RSA_SSA_PSS_SHA256_MGF1RSA-PSS with SHA-2562048+ bits
RSA_SSA_PSS_SHA384_MGF1RSA-PSS with SHA-3842048+ bits
RSA_SSA_PSS_SHA512_MGF1RSA-PSS with SHA-5122048+ bits

ManagedKeyPair

After generating a key, you receive a ManagedKeyPair containing both COSE and JOSE representations. This dual format means you can use the same key material with CBOR-based protocols (like mDL/mdoc) and JSON-based protocols (like JWS) without converting between formats yourself. For hardware-backed keys, the private key fields will be null because the key material never leaves the secure element.

// ManagedKeyPair contains both formats
val keyPair: ManagedKeyPair = keyManager.generateKeyAsync(...)

// Access COSE format
val cosePublicKey = keyPair.cose.publicCoseKey
val cosePrivateKey = keyPair.cose.privateCoseKey // May be null for hardware keys

// Access JOSE/JWK format
val publicJwk = keyPair.jose.publicJwk
val privateJwk = keyPair.jose.privateJwk // May be null for hardware keys

// Key identifiers
val kid = keyPair.kid
val alias = keyPair.alias
val providerId = keyPair.providerId

Key Info Types

Different operations require key info in different encodings. The ManagedKeyInfoType wrapper lets you convert a ManagedKeyPair into a typed key info object scoped to either COSE or JOSE, and with either public or private visibility. Use cborToManagedKeyInfo when working with COSE/CBOR protocols and joseToManagedKeyInfo for JWS/JWE workflows.

// Convert to ManagedKeyInfo for COSE operations
val coseKeyInfo: ManagedKeyInfoType<CoseKeyType> = keyPair.cborToManagedKeyInfo(
visibility = KeyVisibility.PUBLIC
)

// Convert to ManagedKeyInfo for JOSE operations
val joseKeyInfo: ManagedKeyInfoType<JwkType> = keyPair.joseToManagedKeyInfo(
visibility = KeyVisibility.PUBLIC
)

// Generic conversion with encoding specification
val keyInfo = keyPair.toManagedKeyInfo<CoseKeyType>(
visibility = KeyVisibility.PRIVATE,
keyEncoding = KeyEncoding.COSE
)

// Access key info properties
val providerId = keyInfo.providerId
val alias = keyInfo.alias
val signatureAlgorithm = keyInfo.signatureAlgorithm
val x5c = keyInfo.x5c // X.509 certificate chain if available

Signing Data

Create signatures using stored keys:

// Get key info for signing (needs private key access)
val keyInfo = keyPair.cborToManagedKeyInfo(visibility = KeyVisibility.PRIVATE)

// Sign raw bytes
val data = "Hello, World!".encodeToByteArray()
val signature = keyManager.createRawSignature(
keyInfo = keyInfo,
input = data,
requireX5Chain = false
)

// Sign with X.509 certificate chain requirement
val signatureWithCert = keyManager.createRawSignature(
keyInfo = keyInfo,
input = data,
requireX5Chain = true // Requires associated certificate
)

Verifying Signatures

Verify signatures against public keys:

// Get public key info for verification
val publicKeyInfo = keyPair.cborToManagedKeyInfo(visibility = KeyVisibility.PUBLIC)

// Verify the signature
val isValid = keyManager.isValidRawSignature(
keyInfo = publicKeyInfo,
input = data,
signature = signature
)

if (isValid) {
println("Signature is valid")
} else {
println("Signature verification failed")
}

Key Retrieval

You can look up keys by alias (the human-readable name you assigned at generation time) or enumerate all keys in the store. When retrieving a key, you need to supply the provider ID so the key store knows which backend to query. If you only ever use the default provider, keyManager.defaultProviderId() gives you the right value.

Retrieve previously generated keys using the key store:

// List all keys in the key store
val allKeys = keyManager.keyStore.listKeys()
for (keyInfo in allKeys) {
println("Key: ${keyInfo.alias}, Provider: ${keyInfo.providerId}")
}

// Get a specific key
val keyInfo = KeyInfo<CoseKeyType>(
alias = "my-signing-key",
providerId = keyManager.defaultProviderId()
)
val existingKey = keyManager.keyStore.getKey(keyInfo)

Key Deletion

Remove keys that are no longer needed:

// Delete a specific key
val keyInfo = KeyInfo<CoseKeyType>(
alias = "my-signing-key",
providerId = keyManager.defaultProviderId()
)
val deleted = keyManager.keyStore.deleteKey(keyInfo)
if (deleted) {
println("Key deleted successfully")
}

Provider Management

The KeyManagerService can have multiple KMS providers registered simultaneously. For example, you might have a software provider for development and a hardware-backed provider for production. You can look up providers by ID, find one that supports a specific algorithm, or query by capability requirements like hardware backing.

Query and manage KMS providers:

// Get default provider ID
val defaultProvider = keyManager.defaultProviderId()

// List all registered providers
val providerIds = keyManager.getProviderIds()

// Get a specific provider
val provider = keyManager.getProviderById("software")

// Find provider by algorithm support
val ecdsaProvider = keyManager.getKmsBySignatureAlgorithm(SignatureAlgorithm.ECDSA_SHA256)

// Query providers by capabilities
val queryResult = keyManager.queryProvider(
KmsProviderQuery(
signatureAlgorithm = SignatureAlgorithm.ECDSA_SHA256,
requireHardwareBacking = true
)
)

if (queryResult.isOk) {
val provider = queryResult.value.provider
println("Found provider: ${provider.id}")
} else {
println("No matching provider found")
}

Provider Capabilities

Before generating keys or performing operations, you can inspect what each provider actually supports. This is useful when your application runs on multiple platforms where different providers may be available, or when you need to verify that a provider supports a specific curve or key operation before attempting it.

Query what operations a provider supports:

// Get all capabilities from all providers
val capsResult = keyManager.getAllCapabilities(includeDisabled = false)

if (capsResult.isOk) {
for (cap in capsResult.value.capabilities) {
println("Provider: ${cap.providerId}")
println(" Storage types: ${cap.storageTypes.joinToString()}")
println(" Supports import: ${cap.supportsKeyImport}")
println(" Supports export: ${cap.supportsKeyExport}")
println(" Hardware backing: ${cap.supportsHardwareBacking}")
println(" Supported curves: ${cap.supportedCurves.joinToString()}")
println(" Supports signing: ${cap.supportsSigning()}")
println(" Supports encryption: ${cap.supportsEncryption()}")
}
} else {
println("Failed to get capabilities")
}

Key Lifecycle Best Practices

When managing cryptographic keys:

Generate keys with appropriate algorithms. ECDSA_SHA256 (ES256) is a good default for signing, offering a balance of security and performance.

Use meaningful aliases. Include context like purpose and creation date to simplify key management.

Scope keys appropriately. Keys are tied to their provider and cannot be transferred between providers.

Consider key rotation. For long-lived applications, implement key rotation where new keys are generated periodically and old keys retained for verification until they expire.

Use hardware backing when available. The mobile provider uses platform secure storage (iOS Secure Enclave, Android Keystore) which cannot export private keys.