From dd2c407101ee1f1679dfbab4816e8cd6855ca59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Dvo=C5=99=C3=A1k?= Date: Fri, 6 Sep 2024 19:45:17 +0200 Subject: [PATCH 1/2] Fix #640: Add documentation for temporary keys --- docs/Standard-RESTful-API.md | 84 +++++++++++++++++++++++++++++++ docs/Temporary-Encryption-Keys.md | 70 ++++++++++++++++++++++++++ docs/_Sidebar.md | 1 + 3 files changed, 155 insertions(+) create mode 100644 docs/Temporary-Encryption-Keys.md 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** From e08dd67291dcc0b790160fbc6bfc2396b8c6f499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Dvo=C5=99=C3=A1k?= Date: Tue, 17 Sep 2024 17:49:59 +0200 Subject: [PATCH 2/2] Fix #642: Add exact list of used cryptographic standards --- docs/List-of-Used-Algorithms.md | 28 +++++++++++++++++++ docs/_Sidebar.md | 1 + .../powerauth/crypto/lib/util/ByteUtils.java | 2 -- .../crypto/server/util/DataDigest.java | 2 +- .../PowerAuthRequestCanonizationUtils.java | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 docs/List-of-Used-Algorithms.md diff --git a/docs/List-of-Used-Algorithms.md b/docs/List-of-Used-Algorithms.md new file mode 100644 index 000000000..1fd025445 --- /dev/null +++ b/docs/List-of-Used-Algorithms.md @@ -0,0 +1,28 @@ +# List of Used Algorithms + +The following algorithms are used in the PowerAuth cryptography scheme. + +## PowerAuth 3 Protocol + +- Current protocol version: `3.3` + +### Cryptographic Primitives + +| Algorithm | Impacts | Note | +|---------------|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AES-128` | mobile, server | Symmetric encryption with 128 bit keys. Used in `AES/CBC/PKCS7Padding` or `AES/CBC/NoPadding`, depending on use-case. | +| `Argon2` | server | Iterative hash used for storing recovery PUK values associated with recovery codes (`argon2i`). | +| `CRC-16` | mobile, server | Checksum algorithm, used to add a validation to the activation code (2 bytes out of 12 are allocated for checksum). | +| `ECDH` | mobile, server | Key agreement algorithm for ECC-based Diffie-Hellman, uses `secp256r1` curve. | +| `ECDSA` | mobile, server | Asymmetric signatures based on ECC, with `secp256r1` curve and `SHA256` hash function (`SHA256withECDSA`). | +| `ECIES` | mobile, server | Asymmetric encryption scheme based on ECC, with `secp256r1` and `X9.63` (`SHA256`) KDF function. | +| `HMAC-SHA256` | mobile, server | MAC algorithm with `SHA256` as underlying has function. Used in various situations across the protocol. | +| `HMAC-SHA512` | server | MAC algorithm with `SHA256` as underlying has function. Currently only used when validating TOTP in proximity OTP feature. | +| `PBKDF2` | mobile | Derivation function, used with `HMAC-SHA1` algorithm (`PBKDF2WithHmacSHA1`) and 10 000 iterations. _Note: Used exclusively for deriving a symmetric encryption key from PIN code on a mobile device, and hence strength of the algorithm is unimportant._ | +| `SHA256` | mobile, server | Hash function. Used in various situations across the protocol. | +| `X9.63` | mobile, server | Key derivation function with `SHA256`. Used for deriving keys with random index. | + +### Algorithm Providers + +- Server-Side: [Bouncy Castle](https://www.bouncycastle.org/) +- Client-Side: [OpenSSL](https://openssl-library.org/) (libCrypto) \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 46ead8980..a653a6ac0 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -27,6 +27,7 @@ - [Activation Code Format](./Activation-Code.md) - [Additional Activation OTP](./Additional-Activation-OTP.md) - [Implementation Details](./Implementation-notes.md) +- [List of Used Algorithms](./List-of-Used-Algorithms.md) - [List of Used Keys](./List-of-used-keys.md) **Tutorials** diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java index 985ce0d2f..a4e785fa7 100644 --- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java +++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java @@ -43,9 +43,7 @@ public static byte[] concat(byte[]... arrays) { /** * Concatenate multiple byte arrays, including each component size. - * * Sample output byte array structure: [size1][array1][size2][array2] - * * In case byte array is empty, each empty component is encoded as: [0] * * @param arrays Byte arrays to join. diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/util/DataDigest.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/util/DataDigest.java index 9ff71068b..bff3d5a17 100644 --- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/util/DataDigest.java +++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/util/DataDigest.java @@ -108,7 +108,7 @@ public DataDigest(int length) throws GenericCryptoException { * @return Digest fo provided data, including seed used to compute that digest. */ public Result generateDigest(List items) { - if (items.size() == 0) { + if (items.isEmpty()) { return null; } try { diff --git a/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/PowerAuthRequestCanonizationUtils.java b/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/PowerAuthRequestCanonizationUtils.java index bb317a52b..ddaf17e87 100644 --- a/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/PowerAuthRequestCanonizationUtils.java +++ b/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/PowerAuthRequestCanonizationUtils.java @@ -95,7 +95,7 @@ public static String canonizeGetParameters(String queryString) { signatureBaseString.append(URLEncoder.encode(val, StandardCharsets.UTF_8)); } - return signatureBaseString.length() > 0 ? signatureBaseString.toString() : null; + return !signatureBaseString.isEmpty() ? signatureBaseString.toString() : null; } }