api/docs/auth-design.md

18 KiB
Raw Permalink Blame History

auth.chir.rs design document

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Design Goals

  • Authenticate a user to a remote server
  • Provide public key attestation for users
  • Provide key management for end-to-end encryption
  • Prevent token forgery even in the event the authentication server is compromised
  • Directly integrated support for hardware 2FA devices
  • Use modern cryptography whereever possible

Application Server Requirements

Application Servers MUST expose an HTTP Server with application information when accessing the link that is passed as the client ID. The Server MUST support a modern version of TLS1. Support for older versions of TLS is NOT RECOMMENDED2 and the server SHALL NOT support deprecated versions of TLS23. Applications are RECOMMENDED to support HTTP/2 or newer. Application Servers are RECOMMENDED to deploy DNSSEC and DANE TLSA.

Transparency Log

The server maintains a transparency log, which is based on the Gossamer protocol, with the following additional actions:

  1. AttestKey — Provides Attestation between users.
  2. RemoteLogHash — Says that a remote log has been verified for well-formedness until this hash. This does not mean that the log is trustworthy

The transparency log is stored on the content-addressible storage service cas.chir.rs (spec to follow). Each block has the following format:

Name Description
parent The base64 encoded Blake3 hash of the previous block (not present when root block). The previous block can be read by prepending https://cas.chir.rs/ to it.
data A single Gossamer signed message

Example:

{
  "parent": "IF64yd9XgWKAY1aNHwm87Tpbws9zS6SEFM30De1KYUY",
  "data": {
    "...": {}
  }
}

AttestKey

Attests that the key belongs to the provider.

The message MUST contain the following fields:

Name Description
verb MUST be AttestKey
pub Base64-encoded public key to attest
attestation-type See below.
successful true if the attestation was successful, false otherwise4

The attestation type can be one of the following:

Attestation Meaning
in-person The attestor has attempted verification in person
out-of-band The attestor has attempted verification remotely, through an unrelated platform

RemoteLogHash

Says that the remote log has been verified for well-formedness until this hash. This does not mean that the log is trustworthy, but can instead be used for detecting log tampering.

The message MUST contain the following fields:

Name Description
verb MUST be RemoteLogHash
hash Base64-encoded Blake3 hash of the newest checked element
info-url Information URL for the remote log, see the auth api docs

User-Facing protocol

For the end-user, the protocol looks and behaves a lot like OAuth 2.0. This is because it is meant to be compatible with existing IndieAuth clients.

Encryption scheme

Encryption uses XChaCha20 together with Blake35. The keys are derived as follows:

assert len(root_key) == 32
n = csprng(size = 32)
xchacha_seed = blake3_derive_key(
    purpose = "chir.rs-encryption-v0." + b64_urlsafe(n),
    ikm = root_key,
    length = 32 + 24
)
xchacha_key = xchacha_seed[:32]
xchacha_nonce = xchacha_nonce[32:]
blake3_mac_key = blake3_derive_key(
    purpose = b"chir.rs-mac-v0." + b64_urlsafe(n),
    ikm = root_key,
    length = 32
)
commitment = blake3_derive_key(
    purpose = b"chir.rs-key-commitment-v0." + b64_urlsafe(n),
    ukm = root_key,
    length = 32
)

The encryption behaves like a normal non-AEAD cipher mode. The MAC is calculated over the output of the output of the PAE function defined in the Paseto Spec. PAE is used to prevent canonicalization attacks.

The input of the PAE function are, in order:

  • "chir-rs-v0." for URL-Safe encryption, "chir-rs-v0b." for binary encryption
  • The encoded AAD
  • the XChaCha20 ciphertext

The AAD is CBOR-encoded and MUST contain the following fields:

Name Description
non Nonce n, generated above
com Key Commitment value commitment

Additional keys may be included by the application, however all 3 letter keys are reserved for future use.

The output of the URL-Safe encryption will be: "chir-rs-v0." + b64_urlsafe(aad) + "." + b64_urlsafe(ciphertext) + "." + b64_urlsafe(mac)

The output of the binary encryption will be the output of the PAE-function, verbatim, followed by the 32 byte MAC.

Libraries implementing one of these schemes MUST NOT accept a nonce parameter, and MUST generate the nonce using a cryptographically secure pseudorandom number generator

Authentication Client-Server protocol

The Authentication between the client and the server is done in multiple steps.

1: augmented Password-Authenticated Key Exchange

The first step uses the OPAQUE aPAKE with the following cryptographic algorithms:

Name Description
OPRF VOPRF over the ristretto255 group6
KDF, MAC, Cryptographic Hash Blake37
MHF Argon2id8 with m=64MiB, t=8 and p=49
AKE Protocol 3DH10

The aPAKE results in two keys:

  1. A session key Ks, shared between the server and the client
  2. An export key Ke, which only the client knows.

2: Generating the temporary authentication token

To authenticate with the server, the client uses a derived access token to access the remaining endpoints needed for authentication.

The token is derived as follows:

token_bin = blake3_derive_key(
  purpose = b"auth.chir.rs-temp-auth-v0",
  ikm = session_key,
  length = 32
)
token = b64_urlsafe(token_bin)

The token is deleted by the server after 5 minutes or after the final step of the client-server protocol has concluded.

3: Retrieving the Identity Private Key

The server stores the users Identity Private Key encrypted with a key derived from the clients export key and the algorithm described above.

The key is derived as follows:11

key = blake3_derive_key(
  purpose = b"auth.chir.rs-ik-kek-v0",
  ikm = export_key,
  length = 32
)

If the IPK response cannot be decrypted, the account has been reset. The Application MUST tell the user about it, and it MUST either abort, or, with user consent, perform the registration steps outlined below.

4: Generating Authentication Claim

This token contains the requested permissions for the final authentication token.

It is a paseto v4.public token, signed by the long-term-key and MUST contain the following claims:

Name Description
sub The username of the user
aud The client-id of the service
exp Expiration Date (30 days for 1fa tokens, 12 hours for 2fa tokens)
iat Time the token was issued
scp A list of requested scopes

The footer contains the following claim:

Name Description
kid PASERK k4.pid public key id

If the server responds with "204 No Content" at this point, the authentication is complete. Continue at 6. This does not mean that the authentication is successful.

5: 2FA Proofs

TODO: Investigate the options

Bare Ed25519

This is the ideal case and the least brittle to implement. If the user has enabled such a 2fa device, it will be identified using the k4.pid public key id.

The client has to sign a Paseto v4.public token with the authenticators private key. It MUST to include the following claims:

Name Description
wpt The Authentication claim
jti b64_urlsafe(blake3_derive_key(b"auth.chir.rs-2fa-bare-v0", session_key, 32))

The footer contains the following claim:

Name Description
kid PASERK k4.pid public key id

WebAuthn

We support support for P-256 and Ed25519 curves. If the authenticator supports Ed25519, the application MUST use Ed25519. 12

As per the webauthn spec, a challenge, along with other data, is signed by the authenticator.

The challenge signed is a json object with the following properties:

Name Description
wpt The Authentication claim
jti b64_urlsafe(blake3_derive_key(b"auth.chir.rs-2fa-webauthn-v0", session_key, 32))

The data uploaded to the server is a json dictionary containing the following properties:

Name Description
tid Token id, prepended with webauthn-
authenticatorData Authenticator Data, encoded as urlsafe b64
clientDataJson The client Data JSON, encoded as a string
signature The signature, encoded as urlsafe b64

The key is identified like the tid property above.

Android Keystore

Android unfortunately only supports P-256, however luckily this is guaranteed to be supported by the strongbox implementation.

The challenge to be signed is a json document containing the following properties:

Name Description
wpt The authentication claim
jti b64_urlsafe(blake3_derive_key(b"auth.chir.rs-2fa-android-v0", session_key, 32))
kid keystore-b64_urlsafe(blake3(pubkey.get_encoded()))

The key is identified like the kid property above.

Applications MUST use the android keystore system for generating and storing the keypair. They SHALL also use the strongbox implementation the device has and SHALL warn the user if that is not available.

If a future version of the API supports Ed25519 that is also supported by strongbox, applications SHOULD prefer the “Plain Ed25519” variant over this.

6: Returning the authentication code

The authentication code is generated like this and is returned to the requesting application like in oauth2

token_bin = blake3_derive_key(
  purpose = b"auth.chir.rs-auth-code-v0",
  ikm = session_key,
  length = 32
)
token = b64_urlsafe(token_bin)

Client-Server Protocol

The Client MUST NOT accept authentication tokens, and MUST accepts authentication codes. The Client SHOULD invalidate the token if they are not the intended recipient for the token. The Client MUST ask the authentication server for token status.

The Client-Server protocol work analogously to IndieAuth. In addition to a sha256 code verifier, the Client may also use a Blake3 code verifier, named “B3”

Authentication Token Format

The server-signed token is a v4.public PASETO token with the following claims:

Name Description
wtk The Authentication claim, as sent from the client
jti b64_urlsafe(blake3_derive_key(purpose = b"auth.chir.rs-auth-token-v0", ikm=session_key, length=32))
pub The public key of the client in the k4.public format

With the following footer claims:

Name Description
kid PASERK k4.pid public key id

Clients SHOULD verify that the pub claim matches the pub claim of when the user last logged in. Clients SHOULD verify that the clients public key was appended to the transparency log, if the current key is unknown.

If either of these checks fail the client SHOULD perform additional checks:

  1. If the stored key is revoked the Client MUST:
    1. perform a normal account reset procedure, if the data your service manages about the user is public or not sensitive
    2. Delete the account if the data your service manages about the user is sensitive
    3. The server MAY choose to remove any data that may be sensitive and perform a normal account reset procedure
  2. If either the stored or new key is not found on the transparency log, barring technical errors, you MUST disable authentication using auth.chir.rs and publically disclose the discovered mismatch

Federated Scopes

Federated Scopes allow clients to define scopes which the user can grant or reject. To enable the use your client h-app or h-x-app entry must contain an u-x-scopes link to a json file that describe the scopes in a machine-readable format.

Scopes must begin with a reverse-domain-order prefix. I.E. an app with app id https://myapp.example.com/ would start their scopes with com.example.myapp.

The scopes file is a json dictionary with the keys being the scope names. The scope names must begin with your domain name prefix, or be profile or email. The Authentication Client MUST ignore all scopes that are invalid, or depend on invalid scopes. Clients can additionally request scopes starting with rs.chir.auth from any domain.

Each element has the following format:

Key Type Required Description
name LocalizableString true A human-readable name of the scope
description LocalizableString true A more detailed description of the scope
requires List<String> false, defaults to [] List of permissions that need to be granted in addition to this one
sensitive bool false, defaults to false Sensitive scopes have to be manually approved by the user, instead of being selected by default.

LocalizableString

A LocalizableString is a dictionary of language codes. The language code will be, in order of preference:

  • ISO 639-1 code (en, de, ar, zh, etc)
  • ISO 639-1 code, followed by the countrys ISO 3166-1 code (en_US, de_DE, ar_EG, zh_TW, etc)
  • ISO 639-3 code (eng, deu, arz, nan, etc)
  • Glottocode (stan1293, stan1295, egyp1253, minn1241, etc)

If a language does not have any of these identifiers, you could use the Languages canonical name in the Language, except when the name is less than 4 lowercase latin characters, in which case its prefixed by an x-.

Key Management for E2EE

Authenticated clients with the rs.chir.auth.e2e.prekey.write scope can upload signed prekeys and one-time prekeys to the authentication server, which a different authenticated client with the rs.chir.auth.e2e.prekey.read scope can request. This is to facilitate an X3DH key exchange between two peers.

Clients SHALL additionally use Signals Double Ratchet to generate per-message keys.

Unanswered Questions

  • What about group communications? Probably just copy whatever signal does, but with the ciphersuite outlined here.
  • Should end-to-end encrypted messages offer additional security properties such as non-repudiation or deniability? Deniability can likely be achieved by having the recipient leak the previous MAC key in the AAD of their next message.
  • How to handle stored data? Probably ECDHE with the longterm key What about multiple recipients?
  • Should we provide recovery options for the longterm key? With Shamir Secret Sharing I can choose values of the required quorum and the amount of parts so that I can only recover the longterm key if I have access to 1 of my devices. An example would be parts=5, quorum=3, where 1 part is sent to a friend, 1 part is printed out and stored, and the other 3 are stored on my devices. BIP39 would be a good choice for encoding the secret shares in a human-readable way.
  • How would upgrading to newer encryption schemes work?
  • https://github.com/BLAKE3-team/BLAKE3/issues/138? What benefits would this BLAKE3 cipher have over the current XChaCha20-Blake3-based design? Nonce Misuse Resistance, however our current design already requires randomly generated nonce, and bans the API from taking a nonce parameter
  • Should there be explicit API design guidelines for the cryptographic interface?
  • Should it be possible to turn ciphertexts from binary to urlsafe forms without requiring the MAC key?
  • Why CBOR and not Bincode? Bincode is smaller and doesnt store redundant data such as property names Bincode wouldnt neatly support user-defined properties, but the last property could simply be a HashMap<String, Value>, would require additional work defining the layout of HashMap and Value Bincode 2.0 isnt stable yet CBOR would still be needed for webauthn (Would it?)
  • Why JSON PASETO tokens and not CBOR? rusty_paseto currently only supports JSON

  1. “modern” referring to the latest version of TLS or a version that has been superceeded for less than 5 years. Currently this would be TLS 1.3 (latest version) and TLS 1.2 (superceeded in 2018). The rationale for fast deprecation is both that servers that do not support recent versions of TLS are likely out of date and insecure, additionally supporting multiple versions of TLS makes it easier for an attacker to perform a downgrade attack. ↩︎

  2. As of the time of writing, this would be every version of TLS before 1.2. ↩︎

  3. Applications and servers that do not support TLS 1.2 have not been updated in a very long time and are almost certainly insecure. ↩︎

  4. A successful attestation means that the provider was able to verify ownership of the key. A failed attestation means that the provider either verified ownership of a different key, or claimed to not have ownership of the key. The case where the provider rejected the attestation is not considered a failed attestation. ↩︎

  5. This construction provides Random-Key-Robustness and is message-committing. ↩︎

  6. This was chosen as one of the algorithms mentioned in the OPAQUE draft. The reason why Ristretto255 was chosen over P-256 is that it is faster to compute, and also regarded as more secure. ↩︎

  7. Blake3 was chosen for its speed and security. It is a direct successor to Blake2b, but has been tuned for speed. It has no variants ↩︎

  8. Argon2 was chosen because it is less susceptible to tradeoff attacks than for example bcrypt or scrypt. Argon2id was chosen over Argon2i to protect against tradeoff attacks. Argon2id was chosen over Argon2d to protect against side-channel attacks. ↩︎

  9. These numbers were chosen empirically, it takes 3-5s to compute the hash on a Qualcomm SDM636-based smartphone running from battery using WebAssembly on Firefox. ↩︎

  10. This was chosen as the AKE protocol used in the OPAQUE draft. Additionally it is well tested and has been used in multiple end-to-end encrypted protocols. ↩︎

  11. The key is not used directly here, as a future version of this protocol may want to use the key for other purposes ↩︎

  12. Depending on standardization, future versions may accept additional Edwards-Curve based signature algorithms. ↩︎