Message signer
The SDK requires a MessageSigner implementation that proves the device's identity to the cloud relay. Every MPC session (keygen, sign, refresh) starts with the device signing an ephemeral session key — the server verifies the signature against the device's registered public key before allowing the protocol to proceed.
For the SDK contract see Message Signer (Duo cookbook).
Two-class layering
The example splits the signing layer into two classes per platform:
HardwareMessageSigner/SecureEnclaveMessageSigner— implements the SDK'sMessageSignerprotocol. Thin wrapper that delegates to the key manager below.P256KeyManager/SecureEnclaveKeyManager— manages the hardware-backed P-256 (secp256r1) ECDSA key pair. Handles key generation, persistence, and the platform-specific encoding conversions.
The split exists so the SDK contract implementation stays trivial and the platform-specific key management (keystore aliases, encoding formats, StrongBox fallback) is isolated.
Where the signing key lives
- Android
- iOS / macOS
internal class HardwareMessageSigner : MessageSigner {
private val keyManager = P256KeyManager()
override val keyType: MessageSigner.KeyType = MessageSigner.KeyType.ECDSA_P256_SEC1
override val verifyingKey: ByteArray
get() = keyManager.publicKey.clone()
override fun sign(data: ByteArray): ByteArray = keyManager.sign(data)
}
P256KeyManager generates the key pair on first use and stores it in Android Keystore under the alias silentshard_duo_mpc_auth_key. On devices with StrongBox (API 28+), the key lives in a dedicated secure element chip; otherwise it falls back to the TEE. The private key is non-extractable.
Signatures are produced as DER-encoded SHA256withECDSA, then converted to raw r || s (64 bytes) via EllipticCurveEncodingUtils.derSignatureToRaw. The public key is exported as SEC1 uncompressed: 0x04 || x[32] || y[32] (65 bytes).
final class SecureEnclaveMessageSigner: MessageSigner {
private let keyManager: SecureEnclaveKeyManager
init() throws { self.keyManager = try SecureEnclaveKeyManager() }
var keyType: duoinitiator.KeyType { .ecdsaP256Sec1 }
var verifyingKey: Data { keyManager.publicKey }
func sign(_ data: Data) -> Data {
do { return try keyManager.sign(data) }
catch { fatalError("Secure Enclave signing failed: \(error)") }
}
}
SecureEnclaveKeyManager generates the key via SecureEnclave.P256.Signing.PrivateKey() on first use and persists the encrypted key handle to Keychain (kSecAttrAccessibleWhenUnlockedThisDeviceOnly). The encrypted blob can only be decrypted by the same Secure Enclave on the same device.
CryptoKit returns signatures as raw r || s directly (no DER conversion needed) and public keys as x963Representation (SEC1 uncompressed, 65 bytes).