|
1 | 1 | # Activation Status |
2 | 2 |
|
3 | | -PowerAuth Client may need to check for an activation status, so that it can determine if it should display UI for non-activated state (registration form), blocked state (how to unblock tutorial) or active state (login screen). To facilitate this use-case, PowerAuth Standard RESTful API publishes a [/pa/v3/activation/status](./Standard-RESTful-API#activation-status) endpoint. |
| 3 | +PowerAuth Client may need to check for an activation status, so that it can determine if it should display UI for non-activated state (registration form), blocked state (how to unblock tutorial) or active state (login screen). To facilitate this use-case, PowerAuth Standard RESTful API publishes a [/pa/v4/activation/status](./Standard-RESTful-API#activation-status) endpoint. |
4 | 4 |
|
5 | | -Checking for an activation status is simple. Client needs to prepare a HTTP request with an activation ID and random `STATUS_CHALLENGE`. Server processes the request and sends back the response with activation status blob and random `STATUS_NONCE`. Activation status blob is an encrypted binary blob that encodes the activation status. Key `KEY_TRANSPORT` and `STATUS_IV` is used to encrypt the activation blob. |
| 5 | +Checking activation status is performed over standard end-to-end encryption with a temporary activation-scoped key. The legacy STATUS_CHALLENGE / STATUS_NONCE transport is no longer used. |
| 6 | + |
| 7 | +The server returns a Base64-encoded binary activation status blob that is integrity protected with KMAC and transported inside the encrypted response. |
6 | 8 |
|
7 | 9 | ## Status Check Sequence |
8 | 10 |
|
9 | | -The following sequence diagram shows the activation status check in more detail. |
10 | | - |
11 | | - |
12 | | - |
13 | | -## Status Blob Encryption |
14 | | - |
15 | | -1. Both, client and server calculate `KEY_TRANSPORT_IV` as: |
16 | | - ```java |
17 | | - SecretKey KEY_TRANSPORT_IV = KDF.derive(KEY_TRANSPORT, 3000) |
18 | | - ``` |
19 | | -1. Client choose random 16 bytes long `STATUS_CHALLENGE` and send that value to the server: |
20 | | - ```java |
21 | | - byte[] STATUS_CHALLENGE = Generator.randomBytes(16) |
22 | | - ``` |
23 | | -1. Server choose random 16 bytes long `STATUS_NONCE` and calculates `STATUS_IV` as: |
24 | | - ```java |
25 | | - byte[] STATUS_NONCE = Generator.randomBytes(16) |
26 | | - byte[] STATUS_IV_DATA = ByteUtils.concat(STATUS_CHALLENGE, STATUS_NONCE) |
27 | | - byte[] STATUS_IV = KeyConversion.getBytes(KDF_INTERNAL.derive(KEY_TRANSPORT_IV, STATUS_IV_DATA)) |
28 | | - ``` |
29 | | -1. Server uses `KEY_TRANSPORT` as key and `STATUS_IV` as IV to encrypt the status blob: |
30 | | - ```java |
31 | | - encryptedStatusBlob = AES.encrypt(statusBlob, STATUS_IV, KEY_TRANSPORT, "AES/CBC/NoPadding") |
32 | | - ``` |
33 | | -1. Server sends `encryptedStatusBlob` and `STATUS_NONCE` as response to the client. |
34 | | -1. Client receives `encryptedStatusBlob` and `STATUS_NONCE` and calculates the same `STATUS_IV` and then decrypts the status data: |
35 | | - ```java |
36 | | - byte[] STATUS_IV_DATA = ByteUtils.concat(STATUS_CHALLENGE, STATUS_NONCE) |
37 | | - byte[] STATUS_IV = KeyConversion.getBytes(KDF_INTERNAL.derive(KEY_TRANSPORT_IV, STATUS_IV_DATA)) |
38 | | - byte[] statusBlob = AES.decrypt(encryptedStatusBlob, STATUS_IV, KEY_TRANSPORT, "AES/CBC/NoPadding") |
39 | | - ``` |
| 11 | +The client calls the activation status endpoint with an empty request body using standard end-to-end encryption. |
| 12 | + |
| 13 | +Response body (before encryption): |
| 14 | + |
| 15 | +```json |
| 16 | +{ |
| 17 | + "activationStatus": "BASE64", |
| 18 | + "customObject": { |
| 19 | + "_comment": "Any optional service data" |
| 20 | + } |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +Activation status uses activation-scoped end-to-end encryption with SHARED_INFO_1 = "/pa/activation/status". |
40 | 25 |
|
41 | 26 | ## Status Blob Format |
42 | 27 |
|
43 | | -When obtaining the activation status, application receives the binary status blob. Structure of the 32B long status blob is the following (without newlines): |
| 28 | +The final binary status blob is: |
| 29 | + |
| 30 | +```java |
| 31 | +byte[] BINARY_STATUS_BLOB = ByteUtils.concat(STATUS_DATA, STATUS_MAC); |
| 32 | +``` |
| 33 | + |
| 34 | +### STATUS_DATA |
| 35 | + |
| 36 | +Binary layout: |
| 37 | + |
| 38 | +``` |
| 39 | +4B: 0xDEC0DED4 |
| 40 | +1B: ${STATUS} |
| 41 | +1B: ${CURRENT_VERSION} |
| 42 | +1B: ${UPGRADE_VERSION} |
| 43 | +1B: ${STATUS_FLAGS} |
| 44 | +4B: ${RESERVED} |
| 45 | +1B: ${CTR_BYTE} |
| 46 | +1B: ${FAIL_COUNT} |
| 47 | +1B: ${MAX_FAIL_COUNT} |
| 48 | +1B: ${CTR_LOOK_AHEAD} |
| 49 | +32B: ${CTR_DATA_HASH} |
| 50 | +``` |
| 51 | + |
| 52 | +Note: Magic prefix changed from `0xDEC0DED1` to `0xDEC0DED4` to indicate the newest status blob format. |
| 53 | + |
| 54 | +### STATUS_MAC |
| 55 | + |
| 56 | +Integrity protection: |
| 57 | + |
| 58 | +```java |
| 59 | +byte[] STATUS_MAC = Mac.kmac256(KEY_MAC_STATUS, STATUS_DATA, 32, "PA4MAC-STATUS"); |
| 60 | +``` |
| 61 | + |
| 62 | +Where: |
| 63 | + |
| 64 | +```java |
| 65 | +SecretKey KDK_UTILITY = KDF.derive(KEY_ACTIVATION_SECRET, "util"); |
| 66 | +SecretKey KEY_MAC_STATUS = KDF.derive(KDK_UTILITY, "util/mac/status"); |
| 67 | +``` |
| 68 | + |
| 69 | +Note: Even though the blob is delivered over end-to-end encryption, it is additionally authenticated with `STATUS_MAC`. |
| 70 | + |
| 71 | +## Status Fields |
| 72 | + |
| 73 | +### STATUS |
| 74 | + |
| 75 | +- `0x01` – CREATED |
| 76 | +- `0x02` – PENDING_COMMIT |
| 77 | +- `0x03` – ACTIVE |
| 78 | +- `0x04` – BLOCKED |
| 79 | +- `0x05` – REMOVED |
44 | 80 |
|
| 81 | +### CURRENT_VERSION |
| 82 | + |
| 83 | +Current protocol version of the activation (currently `3` or `4`). |
| 84 | + |
| 85 | +### UPGRADE_VERSION |
| 86 | + |
| 87 | +Maximum protocol version supported by the server for this activation (currently `4`). |
| 88 | + |
| 89 | +### STATUS_FLAGS |
| 90 | + |
| 91 | +Bitmask: |
| 92 | + |
| 93 | +- bit 0 – `STATUS_FLAG_ACTIVATION_CONFIRMATION` – pending client confirmation |
| 94 | +- bit 1 – `STATUS_FLAG_UPGRADE_CONFIRMATION` – pending upgrade confirmation |
| 95 | +- bit 2 – `STATUS_FLAG_UNSUPPORTED_ALGORITHM` – activation uses unsupported algorithm |
| 96 | +- bit 3 – `STATUS_FLAG_BIOMETRY_FACTOR_ON` – biometry factor enabled on server |
| 97 | + |
| 98 | +Flag explanation: |
| 99 | + |
| 100 | +Client should treat activation as upgradeable if `CURRENT_VERSION` differs from `UPGRADE_VERSION`. |
| 101 | + |
| 102 | +The `STATUS_FLAG_UNSUPPORTED_ALGORITHM` is used to denote that the algorithm used to create the activation is no longer supported. |
| 103 | + |
| 104 | +Pending activation confirmation is indicated by `STATUS_FLAG_ACTIVATION_CONFIRMATION`. |
| 105 | + |
| 106 | +Pending upgrade confirmation is indicated by `STATUS_FLAG_UPGRADE_CONFIRMATION`. |
| 107 | + |
| 108 | +### CTR_BYTE |
| 109 | + |
| 110 | +Least significant byte of current counter: |
| 111 | + |
| 112 | +```java |
| 113 | +byte CTR_BYTE = (byte)(CTR & 0xFF); |
| 114 | +``` |
| 115 | + |
| 116 | +### Counters |
| 117 | + |
| 118 | +Counter explanation: |
| 119 | + |
| 120 | +- `FAIL_COUNT` = current failed attempts |
| 121 | +- `MAX_FAIL_COUNT` = maximum allowed attempts |
| 122 | +- `CTR_LOOK_AHEAD` = tolerance on the server for counter iterations when the hashed-based counter is ahead on the client |
| 123 | + |
| 124 | +### CTR_DATA_HASH |
| 125 | + |
| 126 | +32-byte hash of the hash-based counter is calculated like this: |
| 127 | + |
| 128 | +```java |
| 129 | +byte[] CTR_DATA_HASH = Mac.kmac256(CTR_DATA, KEY_MAC_CTR_DATA, 32, "PA4MAC-CTR"); |
45 | 130 | ``` |
46 | | -0xDEC0DED1 1B:${STATUS} 1B:${CURRENT_VERSION} 1B:${UPGRADE_VERSION} |
47 | | -5B:${RESERVED} 1B:${CTR_BYTE} 1B:${FAIL_COUNT} 1B:${MAX_FAIL_COUNT} |
48 | | -1B:${CTR_LOOK_AHEAD} 16B:${CTR_DATA_HASH} |
| 131 | + |
| 132 | +The key for counter data is obtained as follows: |
| 133 | + |
| 134 | +```java |
| 135 | +SecretKey KDK_UTILITY = KDF.derive(KEY_ACTIVATION_SECRET, "util"); |
| 136 | +SecretKey KEY_MAC_CTR_DATA = KDF.derive(KDK_UTILITY, "util/mac/ctr-data"); |
49 | 137 | ``` |
50 | 138 |
|
51 | | -where: |
52 | | - |
53 | | -- The first 4 bytes (`0xDE 0xC0 0xDE 0xD1`) are basically a fixed prefix. |
54 | | - - Note that the last byte of this constant also represents the version of the status blob format. If we decide to change the status blob significantly, then the value will be changed to `0xD2`, `0xD3`, etc. |
55 | | -- `${STATUS}` - A status of the activation record, it can be one of following values: |
56 | | - - `0x01 - CREATED` |
57 | | - - `0x02 - PENDING_COMMIT` |
58 | | - - `0x03 - ACTIVE` |
59 | | - - `0x04 - BLOCKED` |
60 | | - - `0x05 - REMOVED` |
61 | | -- `${CURRENT_VERSION}` - 1 byte representing current version of crypto protocol, it can be one of following values: |
62 | | - - `0x02` - PowerAuth protocol version `2.x` |
63 | | - - `0x03` - PowerAuth protocol version `3.x` |
64 | | -- `${UPGRADE_VERSION}` - 1 byte representing maximum protocol version supported by the PowerAuth Server. The set of possible values is identical to `${CURRENT_VERSION}` |
65 | | -- `${RESERVED}` - 5 bytes reserved for the future use. |
66 | | -- `${CTR_BYTE}` - 1 byte representing the least significant byte from current value of counter, calculated as: |
67 | | - ```java |
68 | | - byte CTR_BYTE = (byte)(CTR & 0xFF); |
69 | | - ``` |
70 | | -- `${FAIL_COUNT}` - 1 byte representing information about the number of failed attempts at the moment. |
71 | | -- `${MAX_FAIL_COUNT}` - 1 byte representing information about the maximum allowed number of failed attempts. |
72 | | -- `${CTR_LOOK_AHEAD}` - 1 byte representing constant for a look ahead window, used on the server to validate the authentication code. |
73 | | -- `${CTR_DATA_HASH}` - 16 bytes containing hash from current value of a hash-based counter: |
74 | | - ```java |
75 | | - SecretKey KEY_TRANSPORT_CTR = KDF.derive(KEY_TRANSPORT, 4000); |
76 | | - byte[] CTR_DATA_HASH = KeyConversion.getBytes(KDF_INTERNAL.derive(KEY_TRANSPORT_CTR, CTR_DATA)); |
77 | | - ``` |
| 139 | + |
0 commit comments