From 6843a3980cbc71575062f376748054665bfcb9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Dvo=C5=99=C3=A1k?= Date: Wed, 11 Sep 2024 14:47:19 +0200 Subject: [PATCH] Fix #640: Add documentation for temporary keys (#641) * Fix #640: Add documentation for temporary keys * Add note to E2E encryption * Fix typo --- docs/End-To-End-Encryption.md | 54 +++++++++++--------- docs/Standard-RESTful-API.md | 84 +++++++++++++++++++++++++++++++ docs/Temporary-Encryption-Keys.md | 70 ++++++++++++++++++++++++++ docs/_Sidebar.md | 1 + 4 files changed, 184 insertions(+), 25 deletions(-) create mode 100644 docs/Temporary-Encryption-Keys.md diff --git a/docs/End-To-End-Encryption.md b/docs/End-To-End-Encryption.md index 5bd64a465..8f87f329f 100644 --- a/docs/End-To-End-Encryption.md +++ b/docs/End-To-End-Encryption.md @@ -27,6 +27,9 @@ Assume we have the following constants and variables defined in our scheme: - `KEY_EPH_PUB` - Public part of `EPH_KEYPAIR`. - `SHARED_INFO_2` - Input parameter to MAC calculation. +### Temporary Encryption Keys + +To provide required cryptographic features, such as forward secrecy, encryption uses [temporary encryption keys](./Temporary-Encryption-Keys.md) since protocol version `3.3`. ### Encryption Scope @@ -37,9 +40,9 @@ PowerAuth protocol defines two basic usage scopes for ECIES encryption: #### Application Scope -ECIES in application scope has following configuration of parameters: +ECIES in application scope has the following configuration of parameters: -- `KEY_ENC_PUB` is `KEY_SERVER_MASTER_PUBLIC` +- `KEY_ENC_PUB` is a [temporary key](./Temporary-Encryption-Keys.md) with given `TEMP_KEY_ID` identifier fetched from the server associated with a specific application version and signed with `KEY_SERVER_MASTER_PRIVATE` (to prove it was intended for the application scope). - `SHARED_INFO_1` is a pre-shared constant and is different for each endpoint (see [Pre-shared constants](#pre-shared-constants)) - `SHARED_INFO_2_BASE` is calculated from `APPLICATION_SECRET`: ```java @@ -47,7 +50,7 @@ ECIES in application scope has following configuration of parameters: ``` - `ASSOCIATED_DATA` is calculated as: ```java - byte[] ASSOCIATED_DATA = ByteUtils.concatWithSizes(VERSION, APPLICATION_KEY); + byte[] ASSOCIATED_DATA = ByteUtils.concatWithSizes(VERSION, APPLICATION_KEY, TEMP_KEY_ID); ``` @@ -56,9 +59,9 @@ Note that the `APPLICATION_SECRET` constant is in Base64 form, so we need to rei #### Activation Scope -ECIES in activation scope has following configuration of parameters: +ECIES in activation scope has the following configuration of parameters: -- `KEY_ENC_PUB` is `KEY_SERVER_PUBLIC` (e.g. key which is unique for each activation) +- `KEY_ENC_PUB` is a [temporary key](./Temporary-Encryption-Keys.md) with given `TEMP_KEY_ID` identifier fetched from the server associated with a specific application version and activation, and signed with `KEY_SERVER_PRIVATE` (the key which is unique for each activation, to prove it was intended for the activations cope). - `SHARED_INFO_1` is a pre-shared constant and is different for each endpoint (see [Pre-shared constants](#pre-shared-constants)) - `SHARED_INFO_2_BASE` is calculated from `APPLICATION_SECRET` and `KEY_TRANSPORT`: ```java @@ -66,7 +69,7 @@ ECIES in activation scope has following configuration of parameters: ``` - `ASSOCIATED_DATA` is calculated as: ```java - byte[] ASSOCIATED_DATA = ByteUtils.concatWithSizes(VERSION, APPLICATION_KEY, ACTIVATION_ID); + byte[] ASSOCIATED_DATA = ByteUtils.concatWithSizes(VERSION, APPLICATION_KEY, ACTIVATION_ID, TEMP_KEY_ID); ``` @@ -75,7 +78,7 @@ Note that the `APPLICATION_SECRET` constant is in Base64 form, so we need to rei ### ECIES Encryption -Assume we have a public key `KEY_ENC_PUB`, data `PLAINTEXT` to be encrypted, `ASSOCIATED_DATA` to be included in mac calculation and a `SHARED_INFO_1` and `SHARED_INFO_2_BASE` constants (`byte[]`) as encryption parameters. ECIES encryption works in a following way: +Assume we have a public key `KEY_ENC_PUB`, data `PLAINTEXT` to be encrypted, `ASSOCIATED_DATA` to be included in MAC calculation and a `SHARED_INFO_1` and `SHARED_INFO_2_BASE` constants (`byte[]`) as encryption parameters. ECIES encryption works in the following way: 1. Generate an ephemeral key pair: ```java @@ -111,16 +114,16 @@ Assume we have a public key `KEY_ENC_PUB`, data `PLAINTEXT` to be encrypted, `AS 1. Derive `IV` from `NONCE` and encrypt ata using AES. ```java byte[] IV = KDF_INTERNAL.derive(KEY_IV, NONCE); - byte[] DATA_ENCRYPTED = AES.encrypt(PLAINTEXT, IV, KEY_ENC) + byte[] DATA_ENCRYPTED = AES.encrypt(PLAINTEXT, IV, KEY_ENC); ``` 1. Compute the MAC of encrypted data, include `SHARED_INFO_2`. ```java byte[] DATA = Bytes.concat(DATA_ENCRYPTED, SHARED_INFO_2); - byte[] MAC = Mac.hmacSha256(KEY_MAC, DATA) + byte[] MAC = Mac.hmacSha256(KEY_MAC, DATA); ``` 1. Prepare ECIES payload. ```java - EciesPayload payload = (DATA_ENCRYPTED, MAC, KEY_EPH_PUB, NONCE, TIMESTAMP) + EciesPayload payload = (DATA_ENCRYPTED, MAC, KEY_EPH_PUB, NONCE, TIMESTAMP); ``` If this is a response encryption, then we omit `KEY_EPH_PUB` and set it to `null` in steps 3. and 9. to make the response shorter. For example, `SHARED_INFO_2` is then calculated as: @@ -140,13 +143,13 @@ Assume we have a private key `KEY_ENC_PRIV`, encrypted data as an instance of th ``` 1. Derive base secret key from the private key and ephemeral public key from the ECIES payload (in this step, we do not trim the key to 16b only, we keep all 32b). ```java - SecretKey KEY_BASE = ECDH.phase(KEY_ENC_PRIV, KEY_EPH_PUB) + SecretKey KEY_BASE = ECDH.phase(KEY_ENC_PRIV, KEY_EPH_PUB); ``` 1. Derive a secret key using X9.63 KDF function (using SHA256 internally). When calling the KDF, we use `VERSION`, `SHARED_INFO_1` together with `KEY_EPH_PUB` value (as raw `byte[]`) as an `info` parameter. ```java byte[] VERSION_BYTES = ByteUtils.encode(VERSION); byte[] INFO = Bytes.concat(VERSION_BYTES, SHARED_INFO_1, KEY_EPH_PUB); - SecretKey KEY_SECRET = KDF_X9_63_SHA256.derive(KEY_BASE, INFO, 48) + SecretKey KEY_SECRET = KDF_X9_63_SHA256.derive(KEY_BASE, INFO, 48); ``` 1. Split the 48 bytes long `KEY_SECRET` to three 16B keys. The first part is used as an encryption key `KEY_ENC`. The second part is used as MAC key `KEY_MAC`. The final part is a key for IV derivation `KEY_IV`. ```java @@ -166,7 +169,7 @@ Assume we have a private key `KEY_ENC_PRIV`, encrypted data as an instance of th 1. Decrypt the data using AES, with `IV` value derived from `NONCE`. ```java byte[] IV = KDF_INTERNAL.derive(KEY_IV, NONCE); - byte[] PLAINTEXT = AES.decrypt(DATA_ENCRYPTED, IV, KEY_ENC) + byte[] PLAINTEXT = AES.decrypt(DATA_ENCRYPTED, IV, KEY_ENC); ``` If this is a response decryption, then we omit `KEY_EPH_PUB` and set it to `null` in step 1. @@ -175,9 +178,9 @@ If this is a response decryption, then we omit `KEY_EPH_PUB` and set it to `null Practical implementation of ECIES encryption in PowerAuth accounts for a typical request-response cycle, since encrypting RESTful API requests and responses is the most common use-case. -Client implementation creates an encryptor object that allows encrypting the request and decrypting the response. When encrypting the request, encryptor object accepts a `byte[]` and a public key (for example, `MASTER_SERVER_PUBLIC_KEY`) and produces an instance of `EciesPayload` class. After it receives an encrypted response from the server, which is essentially another instance of `EciesPayload`, it is able to use the original encryption context (the shared encryption keys) to decrypt the response. +Client implementation creates an encryptor object that allows encrypting the request and decrypting the response. When encrypting the request, encryptor object accepts a `byte[]` and a [temporary public key](./Temporary-Encryption-Keys.md) . Then, it produces an instance of `EciesPayload` class. After it receives an encrypted response from the server, which is essentially another instance of `EciesPayload`, it is able to use the original encryption context (the shared encryption keys) to decrypt the response. -Server implementation creates a decryptor object that allows decrypting the original request data and encrypting the response. When server receives an encrypted request, essentially as an `EciesPayload` instance again, it uses a private key (for example, `MASTER_SERVER_PRIVATE_KEY`) to decrypt the original bytes and uses the encryption context to encrypt a response to the client. +Server implementation creates a decryptor object that allows decrypting the original request data and encrypting the response. When server receives an encrypted request, essentially as an `EciesPayload` instance again, it uses a [temporary private key](./Temporary-Encryption-Keys.md) (looked up based on the temporary key ID) to decrypt the original bytes and uses the encryption context to encrypt a response to the client. Since the client and server use the same encryption context, the ephemeral public key needs to be only sent with the request from the client. Response may only contain encrypted data and MAC value. @@ -202,6 +205,7 @@ The typical JSON encoded request is following: ```json { + "temporaryKeyId": "dc497e8a-8faa-44bc-a52a-20d8393005d2", "ephemeralPublicKey" : "A97NlW0JPLJfpG0AUvaRHRGSHh+quZu+u0c+yxsK7Xji", "encryptedData" : "qYLONkDWFpXefTKPbaKTA/PWdRYH5pk9uvGjUqSYbeK7Q0aOohK2MknTyviyNuSp", "mac" : "DNlZdsM1wgH8v2mAROjj3vmQu4DI4ZJnuTBzQMrHsew=", @@ -246,18 +250,18 @@ The response doesn't use HTTP headers. PowerAuth protocol defines following `SHARED_INFO_1` (also called as `sh1` or `sharedInfo1`) constants for its own internal purposes: -| RESTful endpoint | ECIES scope | `SHARED_INFO_1` value | -| ------------------------------------- | ------------ | --------------------- | +| RESTful endpoint | ECIES scope | `SHARED_INFO_1` value | +| ------------------------------------- | ------------ |---------------------------| | `/pa/v3/activation/create` (level 1) | application | `/pa/generic/application` | -| `/pa/v3/activation/create` (level 2) | application | `/pa/activation` | -| `/pa/v3/upgrade` | activation | `/pa/upgrade` | -| `/pa/v3/vault/unlock` | activation | `/pa/vault/unlock` | -| `/pa/v3/token/create` | activation | `/pa/token/create` | -| `/pa/v3/recovery/confirm` | activation | `/pa/recovery/confirm` | +| `/pa/v3/activation/create` (level 2) | application | `/pa/activation` | +| `/pa/v3/upgrade` | activation | `/pa/upgrade` | +| `/pa/v3/vault/unlock` | activation | `/pa/vault/unlock` | +| `/pa/v3/token/create` | activation | `/pa/token/create` | +| `/pa/v3/recovery/confirm` | activation | `/pa/recovery/confirm` | On top of that, following constants can be used for application-specific purposes: -| Purpose | ECIES scope | `SHARED_INFO_1` value | -| ---------------------------------------- | ------------ | --------------------- | +| Purpose | ECIES scope | `SHARED_INFO_1` value | +| ---------------------------------------- | ------------ |---------------------------| | Generic encryptor for application scope | application | `/pa/generic/application` | -| Generic encryptor for activation scope | activation | `/pa/generic/activation` | +| Generic encryptor for activation scope | activation | `/pa/generic/activation` | diff --git a/docs/Standard-RESTful-API.md b/docs/Standard-RESTful-API.md index 0fbef64df..eb3219cc6 100644 --- a/docs/Standard-RESTful-API.md +++ b/docs/Standard-RESTful-API.md @@ -19,6 +19,7 @@ The following endpoints are published in PowerAuth Standard RESTful API (protoco - [`/pa/v3/upgrade/start`](#upgrade-start) - Start a protocol upgrade (requires encryption). - [`/pa/v3/upgrade/commit`](#upgrade-commit) - Commits a protocol upgrade (requires authentication). - [`/pa/v3/recovery/confirm`](#confirm-recovery) - Confirm a recovery code (requires authentication and encryption). +- [`/pa/v3/keystore/create`](#create-new-key-pair) - Create a new temporary key pair for ECIES encryption. ## Security Features @@ -720,3 +721,86 @@ The JSON response after the decryption: } ``` + +## Temporary Keys API + + +### Create New Key Pair + +Create a new temporary key pair with either application or activation scope, and obtain the temporary public for subsequent ECIES encryption. + + +| Request parameter | Value | +| ----------------- |------------------------------------------| +| Method | `POST` | +| Resource URI | `/pa/v3/keystore/create` | + + +#### Request + +##### Body + +The JSON request contains an encoded JWT payload (signed with `HS256`) in a standard request envelope: + +```json +{ + "requestObject": { + "jwt": "..." + } +} +``` + +The decoded content of the JWT payload is: + +```json +{ + "applicationKey" : "...", + "activationId" : "...", + "challenge" : "..." +} +``` + +If the `activationId` is present (and represents an existing activation), the payload represents request for **activation scoped** temporary public key. Otherwise, the payload represents request for **application scoped** temporary public key. The scope determines how the JWT is signed. In both cases, the JWT is signed with standard `HS256` algorithm, with the following secret key: + +- Application scope: Secret key is application secret `APP_SECRET` (decoded to raw bytes). +- Activation scope: Secret key is derived as `KDF_INTERNAL.derive(KEY_TRANSPORT, APP_SECRET)`. + +#### Response 200 + +The JSON response contains an encoded JWT payload (signed with `ES256`) in a standard request envelope: + +```json +{ + "requestObject": { + "jwt": "..." + } +} +``` + +The decoded content of the JWT payload is: + +```json +{ + "sub": "...", + "applicationKey" : "...", + "activationId" : "...", + "challenge" : "...", + "publicKey": "...", + "iat": "...", + "exp": "...", + "iat_ms": "...", + "exp_ms": "..." +} +``` + +- The `sub` claim represents temporary key ID. +- The `applicationKey`, `activationId` and `challenge` claims are the same as in the request, so that the client can validate the response from the server not only for correct signature, but also to ensure the response is related to the issued request. +- The `publicKey` claim represents Base64 encoded temporary public key. +- The `iat` and `exp` attributes are standard claims representing timestamp of JWT issue and expiration timestamp. To provide a millisecond precision, they are augmented with `iat_ms` and `exp_ms` claims. + +The issued public key can be related to either application or activation scope, based on the presence of `activationId` (see the request description for the details). In both cases, the JWT with the public key is signed using `ES256` algorithm, and the scope determines what key is used: + +- Application scope: Private key is the application-specific master server private key `KEY_SERVER_MASTER_PRIVATE`. +- Activation scope: Private key is the activation-specific server private key `KEY_SERVER_PRIVATE`. + + \ No newline at end of file diff --git a/docs/Temporary-Encryption-Keys.md b/docs/Temporary-Encryption-Keys.md new file mode 100644 index 000000000..216f80b25 --- /dev/null +++ b/docs/Temporary-Encryption-Keys.md @@ -0,0 +1,70 @@ +# Temporary Encryption Keys + +To provide better resilience of encryption via advanced features, such as forward secrecy, PowerAuth protocol supports temporary encryption keys (since protocol version 3.3). The idea is that the keys embedded in the mobile app (`KEY_SERVER_MASTER_PUBLIC`) and device specific server public key (`KEY_SERVER_PUBLIC`) are only used for signature verification, serving as trust store on the client for data signed on the server. + +Temporary encryption keys are created on the server side via PowerAuth Standard RESTful API. The server keeps the temporary encryption key pair and the client receives a public key, that can be used in a standard ECIES encryption. + +The client can request two scopes of temporary encryption keys: + +- *Application scope* - the encryption key pair was obtained based on the trust created for the application specific key pair (master server keypair). +- *Activation scope* - the encryption key pair was obtained based on the trust created for the specific activation and it's server key pair (server keypair). + +You can see more information about specific request payloads in [Standard RESTful API documentation](./Standard-RESTful-API.md#temporary-keys-api). + +## Application Scope + +The client sends request in the form of JWT, specifying two parameters: + +- `applicationKey` - key `APP_KEY` associated with the application version +- `challenge` - random challenge, used as a request reference + +The JWT is signed using `HS256` with the "application secret" (`APP_SECRET`) as the signing key. + +The server then takes the request, generates a random temporary encryption key pair associated with the application key, and sends the JWT response signed with `ES256` using `KEY_SERVER_MASTER_PRIVATE`. The JWT response contains: + +- `sub` - identifier of the key +- `applicationKey` - back reference to the original data +- `challenge` - back reference to the original data +- `publicKey` - temporary encryption public key +- `iss` / `iss_ms` - temporary key pair issuance timestamp +- `exp` / `exp_ms` - temporary key pair expiration timestamp + +The client app should process the response by verifying the signature and checking that the application key and challenge match the expected value. Then, the client app can accept the public key with given key identifier. + +## Activation Scope + +The client sends request in the form of JWT, specifying three parameters: + +- `applicationKey` - key `APP_KEY` associated with the application version +- `activationId` - identifier of the specific PowerAuth activation +- `challenge` - random challenge, used as a request reference + +The JWT is signed using `HS256` with the key derived from "application secret" (`APP_SECRET`) and transport key (`KEY_TRANSPORT`) as the signing key: + +``` +JWT_SIGN_KEY = KDF_INTERNAL.derive(KEY_TRANSPORT, APP_SECRET) +``` + +The server then takes the request, generates a random temporary encryption key pair associated with the application key and activation ID, and sends the JWT response signed with `ES256` using `KEY_SERVER_PRIVATE`. The JWT response contains: + +- `sub` - identifier of the key +- `applicationKey` - back reference to the original data +- `activationId` - back reference to the original data +- `challenge` - back reference to the original data +- `publicKey` - temporary encryption public key +- `iss` / `iss_ms` - temporary key pair issuance timestamp +- `exp` / `exp_ms` - temporary key pair expiration timestamp + +The client app should process the response by verifying the signature and checking that the application key, activation ID and challenge match the expected value. Then, the client app can accept the public key with given key identifier. + +## Impacted Use-Cases + +Besides [End-to-End Encryption](./End-To-End-Encryption.md) itself, the introduction of temporary encryption keys impacts all use-cases that implicitly rely on data encryption, such as: + +- New activations (using all supported methods) +- Obtaining and changing activation name from the mobile app. +- Secure Vault +- MAC-based Tokens +- Obtaining User Info +- Confirmation of the Recovery Codes +- Protocol upgrade \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index d4adefdfc..46ead8980 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -19,6 +19,7 @@ **Encryption** - [End-To-End Encryption](./End-To-End-Encryption.md) +- [Temporary Encryption Keys](./Temporary-Encryption-Keys.md) **Other Chapters**