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:
- Android/Kotlin
- iOS/Swift
val keyManager = session.graph.keyManagerService
let 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:
- Android/Kotlin
- iOS/Swift
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
)
import SphereonCrypto
// Generate an ECDSA P-256 signing key
let signingKeyPair = try await keyManager.generateKeyAsync(
providerId: nil, // Use default provider
alias: "my-signing-key",
use: .sig,
keyOperations: [.sign, .verify],
alg: .ecdsaSha256,
keyVisibility: .private_
)
// Generate with a specific provider
let awsKeyPair = try await keyManager.generateKeyAsync(
providerId: "aws",
alias: "my-aws-key",
use: .sig,
alg: .ecdsaSha256
)
Registering an Existing Provider Key
When a key already exists in a tenant-authorized provider, register its reference instead of generating a new key or importing key material. The provider must already be configured, active, and authorized for the tenant. Registration accepts the provider alias and an optional immutable kid. When both are supplied, the service verifies that they resolve to the same provider key.
Registration records the provider identity as Origin.EXTERNAL with EXTERNALLY_MANAGED lifecycle control. It creates no key, imports no key material, and creates no certificate-management record. The private or symmetric key remains in the provider, and cryptographic operations continue to use that provider through the registered reference.
For a tenant-scoped EDK KMS REST deployment, the key registration route is:
POST /api/kms/v1/keys/register
Registration is available only after the tenant provider authorization check. The provider is inspected through a typed, permit-bound provider lease. The service does not use provider-wide enumeration to establish tenant ownership.
Supported Algorithms
The IDK supports the following signature algorithms:
| Algorithm | Description | Curve/Size |
|---|---|---|
ECDSA_SHA256 | ECDSA with SHA-256 | P-256 |
ECDSA_SHA384 | ECDSA with SHA-384 | P-384 |
ECDSA_SHA512 | ECDSA with SHA-512 | P-521 |
RSA_SHA256 | RSA PKCS#1 v1.5 with SHA-256 | 2048+ bits |
RSA_SHA384 | RSA PKCS#1 v1.5 with SHA-384 | 2048+ bits |
RSA_SHA512 | RSA PKCS#1 v1.5 with SHA-512 | 2048+ bits |
RSA_SSA_PSS_SHA256_MGF1 | RSA-PSS with SHA-256 | 2048+ bits |
RSA_SSA_PSS_SHA384_MGF1 | RSA-PSS with SHA-384 | 2048+ bits |
RSA_SSA_PSS_SHA512_MGF1 | RSA-PSS with SHA-512 | 2048+ 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.
- Android/Kotlin
- iOS/Swift
// 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
// ManagedKeyPair contains both formats
let keyPair: ManagedKeyPair = try await keyManager.generateKeyAsync(...)
// Access COSE format
let cosePublicKey = keyPair.cose.publicCoseKey
let cosePrivateKey = keyPair.cose.privateCoseKey // May be nil for hardware keys
// Access JOSE/JWK format
let publicJwk = keyPair.jose.publicJwk
let privateJwk = keyPair.jose.privateJwk // May be nil for hardware keys
// Key identifiers
let kid = keyPair.kid
let alias = keyPair.alias
let 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.
- Android/Kotlin
- iOS/Swift
// 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
// Convert to ManagedKeyInfo for COSE operations
let coseKeyInfo: ManagedKeyInfoType = keyPair.cborToManagedKeyInfo(
visibility: .public_
)
// Convert to ManagedKeyInfo for JOSE operations
let joseKeyInfo: ManagedKeyInfoType = keyPair.joseToManagedKeyInfo(
visibility: .public_
)
// Access key info properties
let providerId = keyInfo.providerId
let alias = keyInfo.alias
let signatureAlgorithm = keyInfo.signatureAlgorithm
let x5c = keyInfo.x5c // X.509 certificate chain if available
The x5c, x5t, and x5t#S256 members are public key and JWKS metadata. Their presence does not register a certificate or create a certificate-management record. When the same public key is projected into a DID verification method JWK, the projection omits the backend provider kid and retains these three public certificate members.
Signing Data
Create signatures using stored keys:
- Android/Kotlin
- iOS/Swift
// 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
)
// Get key info for signing (needs private key access)
let keyInfo = keyPair.cborToManagedKeyInfo(visibility: .private_)
// Sign raw bytes
let data = "Hello, World!".data(using: .utf8)!
let signature = try await keyManager.createRawSignature(
keyInfo: keyInfo,
input: data,
requireX5Chain: false
)
// Sign with X.509 certificate chain requirement
let signatureWithCert = try await keyManager.createRawSignature(
keyInfo: keyInfo,
input: data,
requireX5Chain: true // Requires associated certificate
)
Verifying Signatures
Verify signatures against public keys:
- Android/Kotlin
- iOS/Swift
// 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")
}
// Get public key info for verification
let publicKeyInfo = keyPair.cborToManagedKeyInfo(visibility: .public_)
// Verify the signature
let isValid = try await keyManager.isValidRawSignature(
keyInfo: publicKeyInfo,
input: data,
signature: signature
)
if isValid {
print("Signature is valid")
} else {
print("Signature verification failed")
}
Key Retrieval
You can look up keys by alias or immutable key id and enumerate the references visible to the current tenant. 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. In persistent EDK mode, list operations return tenant-owned references only and do not enumerate unrelated keys from a shared cloud provider.
Retrieve previously generated keys using the key store:
- Android/Kotlin
- iOS/Swift
// 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)
// List all keys in the key store
let allKeys = try await keyManager.keyStore.listKeys()
for keyInfo in allKeys {
print("Key: \(keyInfo.alias), Provider: \(keyInfo.providerId)")
}
// Get a specific key
let keyInfo = KeyInfo(
alias: "my-signing-key",
providerId: keyManager.defaultProviderId()
)
let existingKey = try await keyManager.keyStore.getKey(keyInfo: keyInfo)
Key Deletion
Remove keys that are no longer needed. The result depends on the reference lifecycle mode:
- Android/Kotlin
- iOS/Swift
// 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")
}
// Delete a specific key
let keyInfo = KeyInfo(
alias: "my-signing-key",
providerId: keyManager.defaultProviderId()
)
let deleted = try await keyManager.keyStore.deleteKey(keyInfo: keyInfo)
if deleted {
print("Key deleted successfully")
}
For an EXTERNALLY_MANAGED key, DELETE removes only the tenant-local EDK reference. The provider key remains available and is not sent a delete request. A repeated delete, including one after a process restart, remains local-only because the persisted deleted reference retains its lifecycle mode.
For a PLATFORM_MANAGED key, the existing provider deletion behavior remains in force. The provider resource is deleted according to the provider implementation, and the local reference is updated after that operation succeeds. Do not assume that every key DELETE removes provider material. Check the returned control mode before presenting a destructive lifecycle action.
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:
- Android/Kotlin
- iOS/Swift
// 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")
}
// Get default provider ID
let defaultProvider = keyManager.defaultProviderId()
// List all registered providers
let providerIds = keyManager.getProviderIds()
// Get a specific provider
let provider = keyManager.getProviderById(id: "software")
// Find provider by algorithm support
let ecdsaProvider = keyManager.getKmsBySignatureAlgorithm(
alg: .ecdsaSha256
)
// Query providers by capabilities
let queryResult = try await keyManager.queryProvider(
query: KmsProviderQuery(
signatureAlgorithm: .ecdsaSha256,
requireHardwareBacking: true
)
)
if queryResult.isOk {
let provider = queryResult.value.provider
print("Found provider: \(provider.id)")
} else {
print("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:
- Android/Kotlin
- iOS/Swift
// 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")
}
// Get all capabilities from all providers
let capsResult = try await keyManager.getAllCapabilities(includeDisabled: false)
if capsResult.isOk {
for cap in capsResult.value.capabilities {
print("Provider: \(cap.providerId)")
print(" Storage types: \(cap.storageTypes)")
print(" Supports import: \(cap.supportsKeyImport)")
print(" Supports export: \(cap.supportsKeyExport)")
print(" Hardware backing: \(cap.supportsHardwareBacking)")
print(" Supported curves: \(cap.supportedCurves)")
print(" Supports signing: \(cap.supportsSigning())")
print(" Supports encryption: \(cap.supportsEncryption())")
}
} else {
print("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.
Register references only after provider authorization. Registration identifies a key that already exists in the tenant's provider. It does not create, import, or transfer the key.
Treat external deletion as detachment. EXTERNALLY_MANAGED deletion removes the EDK reference. Use the provider's own lifecycle controls when the provider resource must be retired.
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.