Ports
The host implements these and injects them via WalletPorts. You must supply at least SecureArea,
StorageDriver, and HttpTransport; WalletClock, Rng, WalletLogger, TransactionLogStore, and
WalletAttestationProvider all have defaults (a system clock, the platform secure RNG, no logging, an
in-memory log store, and no attestation, respectively). ProximityTransport is not part of WalletPorts
— you pass it per call to wallet.proximity.present / wallet.reader.read.
SecureArea
The private-key custody boundary: creates and uses key material that never leaves the secure boundary.
Back it with Android Keystore or iOS Secure Enclave; a software implementation exists for tests.
capabilities (SecureAreaCapabilities) declares supported algorithms and whether the area is
hardware-backed and can do user-auth, key attestation, and key agreement — the SDK reads it before, e.g.,
selecting mdoc Mac device auth. Signing over the COSE/JOSE layers is bridged by
SecureAreaCoseSigner / SecureAreaJwsSigner, which call sign for you.
- Kotlin
- Swift
interface SecureArea {
val id: SecureAreaId
val capabilities: SecureAreaCapabilities
suspend fun createKey(spec: KeySpec): KeyInfo
suspend fun publicKey(key: KeyHandle): EcPublicKey
suspend fun sign(key: KeyHandle, algorithm: SigningAlgorithm, data: ByteArray, hint: AuthorizationHint? = null): ByteArray
suspend fun keyAgreement(key: KeyHandle, peerPublicKey: EcPublicKey, hint: AuthorizationHint? = null): ByteArray
/** Null when the area cannot attest (e.g. software area, or a key created without a challenge). */
suspend fun attestation(key: KeyHandle, challenge: ByteArray): KeyAttestation?
suspend fun deleteKey(key: KeyHandle)
}
public protocol SecureArea: Sendable {
var id: SecureAreaId { get }
var capabilities: SecureAreaCapabilities { get }
func createKey(spec: KeySpec) async throws -> KeyInfo
func publicKey(key: KeyHandle) async throws -> EcPublicKey
func sign(key: KeyHandle, algorithm: SigningAlgorithm, data: [UInt8], hint: AuthorizationHint?) async throws -> [UInt8]
func keyAgreement(key: KeyHandle, peerPublicKey: EcPublicKey, hint: AuthorizationHint?) async throws -> [UInt8]
/// Nil when the area cannot attest (e.g. software area, or a key created without a challenge).
func attestation(key: KeyHandle, challenge: [UInt8]) async throws -> KeyAttestation?
func deleteKey(key: KeyHandle) async throws
}
An adapter qualifies by passing SecureAreaContract.verify(area) from the test kit.
StorageDriver
Byte persistence keyed by collection + key, plus a transaction scope. Wrap it with encryption at rest for production.
- Kotlin
- Swift
interface StorageDriver {
suspend fun put(collection: String, key: String, value: ByteArray)
suspend fun get(collection: String, key: String): ByteArray?
suspend fun delete(collection: String, key: String)
suspend fun keys(collection: String): List<String>
suspend fun transaction(block: suspend StorageTx.() -> Unit)
}
public protocol StorageDriver: Sendable {
func put(collection: String, key: String, value: [UInt8]) async throws
func get(collection: String, key: String) async throws -> [UInt8]?
func delete(collection: String, key: String) async throws
func keys(collection: String) async throws -> [String]
func transaction(_ block: (any StorageTx) async throws -> Void) async throws
}
HttpTransport
Must honour request.followRedirects — the OpenID flows depend on intercepting redirects.
- Kotlin
- Swift
interface HttpTransport {
suspend fun execute(request: HttpRequest): HttpResponse
}
public protocol HttpTransport: Sendable {
func execute(_ request: HttpRequest) async throws -> HttpResponse
}
ProximityTransport
A duplex framed-message channel for ISO 18013-5 (see Proximity). The SDK
drives the message exchange; you supply the BLE/NFC radio. retrievalMethods() / nfcCarrier() let
the transport tell the SDK its BLE carrier (UUID + mode) for the QR / NFC engagement — both default to
none, so a minimal transport implements only the three duplex methods.
- Kotlin
- Swift
interface ProximityTransport {
suspend fun send(message: ByteArray)
suspend fun receive(): ByteArray
suspend fun close()
fun retrievalMethods(): List<ByteArray> = emptyList() // BLE DeviceRetrievalMethod(s) for QR engagement
fun nfcCarrier(): NfcCarrier? = null // BLE carrier for NFC static handover
}
public protocol ProximityTransport: Sendable {
func send(_ message: [UInt8]) async throws
func receive() async throws -> [UInt8]
func close() async
func retrievalMethods() -> [[UInt8]] // BLE DeviceRetrievalMethod(s) for QR engagement
func nfcCarrier() -> NfcCarrier? // BLE carrier for NFC static handover
}
TransactionLogStore
Append-only audit persistence — the facade wraps it as wallet.transactions.
- Kotlin
- Swift
interface TransactionLogStore {
suspend fun append(entry: TransactionLogEntry)
suspend fun all(): List<TransactionLogEntry>
}
public protocol TransactionLogStore: Sendable {
func append(_ entry: TransactionLogEntry) async
func all() async -> [TransactionLogEntry]
}
WalletAttestationProvider
Optional — the link to your Wallet Provider backend. walletAttestation returns a Wallet Unit
Attestation (WUA) used for attestation-based client authentication during issuance; keyAttestation
returns a per-issuance key attestation over the holder keys the issuer will bind credentials to.
- Kotlin
- Swift
interface WalletAttestationProvider {
suspend fun walletAttestation(keyInfo: KeyInfo): String
suspend fun keyAttestation(keys: List<KeyInfo>, nonce: String?): String
}
public protocol WalletAttestationProvider: Sendable {
func walletAttestation(keyInfo: KeyInfo) async throws -> String
func keyAttestation(keys: [KeyInfo], nonce: String?) async throws -> String
}
Runtime ports
WalletClock, Rng, and WalletLogger are small runtime hooks. The clock and RNG are injectable so
tests can pin them for deterministic validity checks and transcripts; both default to the platform
implementation. WalletLogger is a structured sink for the SDK's and adapters' logs — default none.
- Kotlin
- Swift
fun interface WalletClock { fun now(): Instant } // default: WalletClock.System
fun interface Rng { fun nextBytes(size: Int): ByteArray } // default: Rng.Default
interface WalletLogger {
enum class Level { Debug, Info, Warn, Error }
fun log(level: Level, message: String, throwable: Throwable? = null)
}
public protocol WalletClock: Sendable { func now() -> Date } // default: SystemClock()
public protocol Rng: Sendable { func nextBytes(_ size: Int) -> [UInt8] } // default: SystemRng()
public enum LogLevel: Sendable { case debug, info, warn, error }
public protocol WalletLogger: Sendable {
func log(level: LogLevel, message: String, error: Error?)
}