explainers

Explainer: Remote CryptoKeys

Authors: Jon Choukroun, Simon Gornall, Michael Hashe, Andy Pace, Marcos Cáceres.

Problem

Supporting end-to-end encryption on the web currently requires making significant compromises. Implementations may execute cryptographic operations in the client, potentially exposing a user’s private key to JavaScript, which makes keys vulnerable to exfiltration.

Alternative implementations may seek to protect the keys by executing cryptographic operations in other environments, such as the server, which forfeits the guarantees expected from end-to-end encryption.

Finally, implementations may preserve the security and ownership of private keys, which can complicate the user experience (e.g., by requiring the installation of browser extensions).

Goals

Non-Goals

Proposal

We identified 4 requirements that informed the design of this proposal:

  1. Private key material is never directly exposed to JavaScript, but can be used to perform cryptographic operations with the user’s consent.
  2. Private key ownership is exclusive to the user - keys are not escrowed, cryptographic operations are not executed by a 3rd party.
  3. Malicious web sites cannot use this feature to track users (e.g., via a digital fingerprint or the creation of a super cookie).
  4. No additional user experience burden.

The rest of this document illustrates how end-to-end encryption on the web can be supported using minimal extensions to the Web Cryptography API to support executing cryptographic operations (encrypt, decrypt, sign, and verify) using keys from a user’s secure key store.

System overview flow chart

We propose a mechanism through which a web application can request that a cryptographic operation be executed on a given data chunk, using the following process:

  1. The web application first acquires an instance of a “remote” CryptoKey by calling getRemoteKey(), detailed below.

    Note: This is analogous to calling generateKey() to instantiate a local CryptoKey.

  2. The web application passes this CryptoKey as an argument to methods in the .subtle namespace, along with the data to sign, decrypt, etc.
  3. The browser then passes the CryptoKey and data to the platform. This may be the default key store, an entity managing one or more key stores, or some other process running on the operating system.
    1. The device may have more than one such process, it is up to the browser to select one - with user preference guiding the choice.
  4. This platform process makes the appropriate access control and user consent checks, and searches for a matching key.
  5. If the key is found, the process will execute the requested cryptographic operation on the given data, and return the output to the browser.
  6. If the key is not found, or access control checks fail, the process will return an error to the browser.
    1. Returning the same error for both of these cases mitigates a privacy vulnerability where a web application could check on the existence of a key.

RemoteKeyParams Dictionary

We propose defining a kind of “remote” key that is usable with CryptoKey ‘s [[algorithm]] internal slot. That is, the key material exists outside the browser on the platform or elsewhere. This may be a secure key store, password manager, USB or BlueTooth device, etc.

When creating a remote instance of a CryptoKey, the browser sets its attributes accordingly:

// The [[algorithm]] internal slot takes a RemoteKeyParams
dictionary RemoteKeyParams : Algorithm {
  DOMString userIdentifier;
  DOMString? keyId;
  DOMHighResTimeStamp? expiresAt;
}

Extensions to the SubtleCrypto Interface

The intention of this proposal is to leverage the existing SubtleCrypto namespace. A remote instance of the CryptoKey can be passed as the key argument to existing cryptographic methods without requiring an API change.

To support getting a CryptoKey handle to remote key material, we propose a new method, an alternative to the existing generateKey() function. This method takes a RemoteKeyParams dictionary (see above) and an array of key usages. Because remote keys are never extractable, we omit the extractable boolean parameter from the method signature:

Promise<CryptoKey> getRemoteKey(
  RemoteKeyParams params, sequence<KeyUsage> keyUsages
);

We propose the following implementation of getRemoteKey():

  1. When called, getRemoteKey() receives RemoteKeyParams and keyUsages parameters, otherwise returns a rejected Promise with some error.
  2. Return a Promise, and run the following steps in parallel.
  3. The browser wraps the existing parameters with the origin of the requesting web application.
  4. The browser communicates with the platform, passing the parameters to match an existing key.
  5. The browser will need to implement some UI for choosing one out of possibly several key providers. User preferences should guide this choice.
  6. How the platform handles access control and user consent checks is out of scope for this proposal. However any response that isn’t a success should reject the Promise with a NotFoundError.
  7. On success, the platform should return a unique identifier which can be used for subsequent key lookups.
  8. The browser then creates a CryptoKey instance, setting the identifier returned from the platform as the keyId member, and the expiration time as the expiresAt member - if provided.
  9. A platform process may choose not to return these values, forcing subsequent cryptographic calls through the SubtleCrypto API to do a full key search.
  10. Finally, resolve the Promise with this CryptoKey object.

Examples

Getting a CryptoKey handle, to be later used to sign an email message body.

const keyHandle = await window.crypto.subtle.getRemoteKey(
    {
        name: "remote",
        userIdentifier: "alice@example.com"
    },
    ["sign"]
);

console.log(keyHandle.algorithm.keyId) // a platform-unique identifier
console.log(keyHandle.algorithm.expiresAt) // a timestamp 3 hours from now
console.log(keyHandle.extractable) // always "false"
console.log(keyHandle.usages) // "sign"

Using the previously retrieved key handle to sign an email message.

// Dummy function that assembles message contents into a bytes array
// @type ArrayBuffer
const message = assembleMessage();

// @type ArrayBuffer
const signature = await window.crypto.subtle.sign(
    { name: "RSASSA-PKCS1-v1_5" },
    keyHandle,
    message
);

Security & Privacy Considerations

This proposal focuses on obtaining access to key handles, but does not dictate the access control and user consent checks that the platform should enforce. Instead we will list concerns and possible mitigations (where possible) that a platform SHOULD implement to better protect these keys.