Skip to main content

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's MessageSigner protocol. 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

vault/signing/HardwareMessageSigner.kt
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).