Skip navigation
Duo Labs

Humans Only: Duo Mobile and Android Protected Confirmation

Duo has been working with Google since late last year on the Android Protected Confirmation API available in Android P, helping provide Google with feedback as part of Google’s early technology preview program.

We’ve prototyped an integration with this new security API in Duo Mobile to evaluate its capabilities, as well as consider it for potential inclusion in our product later this year when Android P and compatible hardware is released to the public.

How Does Android Protected Confirmation Work?

Android Protected Confirmation provides cryptographic assurance of human presence and intent when responding to a prompt or dialog. This prevents the on-screen prompt from being hijacked or clicked by a malicious application. We immediately saw appropriate use cases in our product, such as gaining greater confidence that a human was involved in responding to a Duo Push notification, one way to complete two-factor authentication and verify their identity.

Android Protected Confirmation is also a good fit for financial or healthcare applications. Imagine the benefits of being able to confirm that a human – not malware running on the device – was present before transferring money? Or before injecting insulin? More applications of this technology will undoubtedly come to light in the coming months and years.

Technical Overview

The Android Protected Confirmation API specifies that a key pair is generated on a device in the Trusted Execution Environment (TEE), with a specific CONFIRMATION tag that limits its use. We also generate, store, and send an attestationChallenge from the back-end server, which is used when generating the key pair on the device.

The public key is sent to the application back end during registration, and if deemed to be trustworthy, is used in the future to verify a Confirmation signature for each authentication. A valid Confirmation signature accompanying an authentication response ensures that a human responded to the transaction from the enrolled device, not malware written by an adversary, or software attempting to automatically respond to received authentication requests.

Android Protected Confirmations

Android Protected Confirmation requires Android P or greater, and a device with a compatible TEE. In order for us to build a functional prototype that is API compatible with Android Protected Confirmation, we used a shim library for Android which opportunistically calls into the com.android.security.Confirmation API on compatible devices.

On the back end, no communication with Google servers is necessary. This allows for signatures to be quickly validated instead of requiring extra round-trip time to Google’s services. All that’s required is to have the proper Android Key Attestation root certificate from Google to verify the Confirmation public key, as well as a cryptographic library that can handle validating the Confirmation signature. The back end not only ensures that the Confirmation certificate chain is valid and was created with the appropriate CONFIRMATION tag, but that the root certificate is a Google certificate.

Technical Architecture

As previously mentioned, the Android Protected Confirmation API is a natural fit for Duo Push authentication, since it’s used to validate that a user is who they say they are. In our prototype, the Confirmation API is used when a Duo Push is received, which gives us greater assurance that the user approved the transaction. The diagram below shows the flow for Android Protected Confirmation usage in Duo.

Android Protected Confirmation Flow Click to view larger.

How Android Protected Confirmation Works on the Server

Here’s how Android Protected Confirmation works on supported Android Devices running Android P or greater:

  • First, we determine if a new Android Protected Confirmation key pair needs to be generated. This typically happens when Duo Mobile launches.
  • The public key is sent to the server

Then, the server validates that:

If the Confirmation public key is successfully validated, we store it in the server-side database for the enrolling device.

Below is a Python code sample showing how to verify the attestationChallenge of the leaf certificate, or leaf, from the certificate chain.

from asn1crypto.core import OctetString
from asn1crypto.core import Sequence
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import ObjectIdentifier

attestation_challenge = '' # Pull this from Redis, for example.
decoded_challenge = attestation_challenge.decode('base64')
actual_challenge = ''

cert = x509.load_der_x509_certificate(
    leaf.decode('base64'), default_backend())
oid = ObjectIdentifier('1.3.6.1.4.1.11129.2.1.17')
extension = cert.extensions.get_extension_for_oid(oid)

if isinstance(extension, x509.extensions.Extension):
    value = extension.value
    if isinstance(value, x509.extensions.UnrecognizedExtension):
        asn1_sequence = Sequence.load(value.value)
        if len(asn1_sequence) >= 5:
            challenge = asn1_sequence[4]
            if isinstance(challenge, OctetString):
                actual_challenge = challenge.native

return actual_challenge == decoded_challenge

Once the leaf certificate’s attestationChallenge has been verified, the server can continue certificate chain validation.

Below is a code sample in Python showing how to perform the certificate chain validation and check that the root certificate is a Google root certificate.

from OpenSSL import crypto

TRUSTED_GOOGLE_ROOT = '<GOOGLE_ROOT_CERTIFICATE_PEM>'

ROOT_FROM_CHAIN = intermediates[-1]
store_ctx = crypto.X509StoreContext(_CERT_STORE, leaf, intermediates[:-1])

try:
    store_ctx.verify_certificate()
except crypto.X509StoreContextError as e:
    return False

if TRUSTED_GOOGLE_ROOT and ROOT_FROM_CHAIN:
    root_from_chain_pem = crypto.dump_certificate(
        crypto.FILETYPE_PEM,
        ROOT_FROM_CHAIN).strip()
    if root_from_chain_pem == TRUSTED_GOOGLE_ROOT:
        return True

return False

Once an Android Protected Confirmation key pair has been created for a compatible Android P device, the Confirmation public key is used to validate each Duo Push authentication received from the device.

When a Duo Push notification is sent to an enrolled device, it includes a nonce, along with a promptText string to be displayed in the Confirmation dialog. The promptText and nonce are stored temporarily on the server to be retrieved during signature validation.

When the user taps the notification on their enrolled device to respond to the Duo Push, the Android Protected Confirmation Prompt containing promptText is displayed to the user. The user then responds to the Android Protected Confirmation dialog by clicking either Approve or Deny which generates and sends a signature to the server.

When the server receives this data, along with the response, the previously-stored Android Protected Confirmation public key is used to verify the signature over (promptText, nonce). If the signature is valid, the answer is Approve, and the promptText and nonce match what is expected, then the Duo service can trust that the push authentication was approved by a real user, and the authentication flow can continue.

If any of these conditions are not met, the authentication attempt cannot be trusted and fails. Below is a code sample in Python showing how to check that promptText and nonce are as expected, and that the Confirmation signature is valid.

confirmations_signature = base64.b64decode(
    encoded_confirmations_signature)
confirmations_data_that_was_confirmed = base64.b64decode(
    encoded_confirmations_data_that_was_confirmed)
data_that_was_confirmed = cbor2.loads(confirmations_data_that_was_confirmed)

extra_data = ''  # Pull this from Redis, for example.
prompt_text = ''  # Pull this from Redis, for example.
received_prompt_text = data_that_was_confirmed.get('prompt')
received_extra_data = data_that_was_confirmed.get('extra')

if not received_prompt_text or not received_extra_data:
    raise Exception('Missing prompt_text and/or extra_data.')
if prompt_text != received_prompt_text:
    raise Exception('Invalid Android Protected Confirmations prompt_text.')
if extra_data != received_extra_data:
    raise Exception('Invalid Android Protected Confirmations extra_data.')

confirmations_pub_key = ''  # Pull the device public key from the database.
if not confirmations_pub_key:
    raise Exception('Missing confirmations_pub_key.')

confirmations_pub_key = confirmations_pub_key.encode('ascii')
pk = load_pem_public_key(confirmations_pub_key, backend=default_backend())

verifier = pk.verifier(confirmations_signature, ECDSA(SHA256()))
verifier.update(confirmations_data_that_was_confirmed)
try:
    verifier.verify()
except InvalidSignature:
    raise Exception('Invalid Android Protected Confirmations signature received.')

It should be noted that the data passed to the back end from the Android Protected Confirmation API is in Concise Binary Object Representation (CBOR) format. CBOR uses a data model based on JavaScript Object Notation (JSON), but is a binary format. Or, as my colleague Nick Steele says, “CBOR is like JSON for smug people.”

How Android Protected Confirmation Works on Android Devices

On supported devices running Android P or greater, an Android Protected Confirmation key pair is created when an account is activated or updated by receiving an attestationChallenge. The public key is then sent to the back end for verification, and stored in the database with the device record. The code sample below shows how the key pair is generated.

KeyStore ks = KeyStore.getInstance(KEY_STORE_NAME);
ks.load(null);

String alias = makeNewAlias();
KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEY_STORE_NAME);

kpg.initialize(new KeyGenParameterSpec.Builder(
    alias,
    KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
    .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
    .setAttestationChallenge(attestationChallengeBytes)
    .setUserConfirmationRequired(true)
    .build());
KeyPair kp = kpg.generateKeyPair();

If an Android Protected Confirmation key pair is present on the device, and the Duo account administrator has enabled the Android Protected Confirmation feature in the Duo Admin Panel, then from now on when a Duo Push is received, Duo Mobile will present the Android Protected Confirmation dialog with the promptText received from the back end. This is handled by the com.android.security.Confirmation* classes.

The Secure World, (generally the Trusted Execution Environment, or TEE), takes over input/output and displays the prompt to the user. The user is then able to approve or deny the Android Protected Confirmation Prompt with their response going directly to the TEE. This ability for the user to make use of the TEE ensures user presence and intent to approve a Duo Push, and thus authenticate into a Duo-protected application.

The Android Keystore System can optionally provide assurance that keys never leave secure hardware, but when combined with Android Protected Confirmation, we gain additional guarantees that the private key is only accessible to a human using the device. To transfer the proof of user presence, a signature is created over (promptText, nonce) using the Android Protected Confirmation private key. The Confirmation prompts are implemented such that a response provides high confidence that the user has seen the prompt, even if the Android framework (including the kernel) is compromised.

It’s important to note that hardware support is required to implement Confirmation prompts with these guarantees. Dedicated hardware may not be present on all devices, in which case the level of assurance of user presence would diminish. When a user responds to the Confirmation prompt, the TEE signs the request with the Android Protected Confirmation private key. We expect future Android devices to ship with the hardware required to support Android Protected Confirmation.

If the user approves the Duo Push authentication, a CBOR data structure is returned that consists of (promptText, nonce), as well as a signature over that data created with the Confirmation private key. Duo Mobile then sends the CBOR data structure and signature to the back end. The code sample below shows how the signature is generated over the data.

byte[] signature = Confirmations.signDataThatWasConfirmed(dataThatWasConfirmed, confirmationsKeyAlias);
String encodedDataThatWasConfirmed = Base64.encodeToString(dataThatWasConfirmed, Base64.NO_WRAP);
String encodedSignature = Base64.encodeToString(signature, Base64.NO_WRAP);
// Send encodedSignature and encodedDataThatWasConfirmed to the server for verification.

If the data sent to the back end is as expected, and the signature over that data is valid, the authentication is approved. Otherwise the authentication fails.

Summary

We're excited about the future of Android Protected Confirmation at Duo. Our collaboration with Google to preview early features is an example of our commitment to innovation and evaluating new security technologies all while sharing our learnings with the wider security industry.

We thank Google for allowing Duo the opportunity to prototype an integration with this API in Duo Mobile and to be part of the announcement at Google I/O. Google has been very receptive to our feedback, and we’re excited about the future of Android Protected Confirmation.

This is something we are considering for future inclusion in our product to help administrators protect high-assurance applications and high-risk users. We’ll have more to share on this once Android P and compatible devices become generally available.

In the meantime, be sure to follow @duo_labs to stay updated on our latest developments, and stay tuned for our Google I/O recap blog post coming next week.