Merge pull request #75 from NicolasDorier/qfointq

Remove the need to generate random CardKey
This commit is contained in:
Peter Rounce 2023-10-28 12:52:52 +01:00 committed by GitHub
commit e60705ba49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -1,17 +1,17 @@
## Abstract ## Abstract
The NXP NTAG424DNA allows applications to configure five application keys, named `K0`, `K1`, `K2`, `K3`, and `K4`. In the Bolt card configuration: The NXP NTAG424DNA allows applications to configure five application keys, named `K0`, `K1`, `K2`, `K3`, and `K4`. In the BoltCard configuration:
* `K0` is the `App Master Key`, it is the only key permitted to change the application keys. * `K0` is the `App Master Key`, it is the only key permitted to change the application keys.
* `K1` serves as the `encryption key` for the `PICCData`, represented by the `p=` parameter. * `K1` serves as the `encryption key` for the `PICCData`, represented by the `p=` parameter.
* `K2` is the `authentication key` used for calculating the SUN MAC of the `PICCData`, represented by the `c=` parameter. * `K2` is the `authentication key` used for calculating the SUN MAC of the `PICCData`, represented by the `c=` parameter.
* `K3` and `K4` are not used but should be configured as recommended in the [NTag424 application notes](https://www.nxp.com/docs/en/application-note/AN12196.pdf). * `K3` and `K4` are not used but should be configured as recommended in the [NTag424 application notes](https://www.nxp.com/docs/en/application-note/AN12196.pdf).
A simplistic approach to issuing Bolt cards would involve randomly generating the five different keys and storing them in a database. A simple approach to issuing BoltCards would involve randomly generating the five different keys and storing them in a database.
When a validation request is made, the verifier would attempt to decrypt the `p=` parameter using all existing encryption keys until finding a match. Once decrypted, the `p=` parameter would reveal the card's uid, which can then be used to retrieve the remaining keys. When a validation request is made, the verifier would attempt to decrypt the `p=` parameter using all existing encryption keys until finding a match. Once decrypted, the `p=` parameter would reveal the card's uid, which can then be used to retrieve the remaining keys.
The primary drawback of this method is its lack of scalability. If many cards have been issued, identifying the correct encryption key could become computationally expensive. The primary drawback of this method is its lack of scalability. If many cards have been issued, identifying the correct encryption key could become a computationally intensive task.
In this document, we propose a solution to this issue. In this document, we propose a solution to this issue.
@ -19,43 +19,55 @@ In this document, we propose a solution to this issue.
First, the `LNUrl Withdraw Service` generates a `IssuerKey` that it will use to generate the keys for every NTag424. First, the `LNUrl Withdraw Service` generates a `IssuerKey` that it will use to generate the keys for every NTag424.
Then configure a Boltcard the following way: Then, configure a BoltCard as follows:
* `CardKey = GetRandomBytes(16)` * `CardKey = PRF(IssuerKey, '2d003f75' || UID || Version)`
* `K0 = PRF(CardKey, '2d003f76' || UID)` * `K0 = PRF(CardKey, '2d003f76')`
* `K1 = PRF(IssuerKey, '2d003f77')` * `K1 = PRF(IssuerKey, '2d003f77')`
* `K2 = PRF(CardKey, '2d003f78' || UID)` * `K2 = PRF(CardKey, '2d003f78')`
* `K3 = PRF(CardKey, '2d003f79' || UID)` * `K3 = PRF(CardKey, '2d003f79')`
* `K4 = PRF(CardKey, '2d003f7a' || UID)` * `K4 = PRF(CardKey, '2d003f7a')`
* `ID = PRF(IssuerKey, '2d003f7b' || UID)`
With the following parameters:
* `IssuerKey`: This 16-bytes key is used by an `LNUrl Withdraw Service` to setup all its BoltCards.
* `UID`: This is the 7-byte ID of the card. You can retrieve it from the NTag424 using the `GetCardUID` function after identification with K1, or by decrypting the `p=` parameter, also known as `PICCData`. * `UID`: This is the 7-byte ID of the card. You can retrieve it from the NTag424 using the `GetCardUID` function after identification with K1, or by decrypting the `p=` parameter, also known as `PICCData`.
* `Version`: A 4-bytes little endian version number. This must be incremented every time the user re-programs (reset/setup) the same BoltCard on the same `LNUrl Withdraw Service`.
The Pseudo Random Function `PRF(key, message)` applied during the key generation is the CMAC algorithm described in NIST Special Publication 800-38B. [See implementation notes](#notes) The Pseudo Random Function `PRF(key, message)` applied during the key generation is the CMAC algorithm described in NIST Special Publication 800-38B. [See implementation notes](#notes)
## How to setup a new boltcard ## How to setup a new BoltCard
1. Generate a random `CardKey` of 16 bytes. 1. Execute `ReadData` or `ISOReaDBinary` on the BoltCard to ensure the card is blank.
2. `ReadData` or `ISOReaDBinary` on the boltcard, to make sure the card is blank. 2. Execute `AuthenticateEV2First` with the application key `00000000000000000000000000000000`
3. Execute `AuthenticateEV2First` with `00000000000000000000000000000000` 3. Fetch the `UID` with `GetCardUID`.
4. Fetch the `UID` with `GetCardUID`. 4. Calculate `ID`
2. Calculate `K0`, `K1`, `K2`, `K3`, `K4`. 5. Fetch the `State` and `Version` of the BoltCard with the specified `ID` from the database.
4. [Setup the boltcard](./CARD_MANUAL.md). 6. Ensure either:
* If no BoltCard is found, insert an entry in the database with `Version=0` and its state set to `Configured`.
* If a BoltCard is found and its state is `Reset` then increment `Version` by `1`, and change its state to `Configured`.
7. Generate `CardKey` with `UID` and `Version`.
8. Calculate `K0`, `K1`, `K2`, `K3`, `K4`.
9. [Setup the BoltCard](./CARD_MANUAL.md).
## How to implement a Reset feature ## How to implement a Reset feature
If a `LNUrl Withdraw Service` offers a factory reset feature for a user's bolt card, here is the recommended procedure: If a `LNUrl Withdraw Service` offers a factory reset feature for a user's BoltCard, here is the recommended procedure:
1. Read the NDEF lnurlw URL, extract `p=` and `c=`. 1. Read the NDEF lnurlw URL, extract `p=` and `c=`.
2. Derive `Encryption Key (K1)`, decrypts `p=` to get the `PICCData`. 2. Derive `Encryption Key (K1)`, decrypt `p=` to obtain the `PICCData`.
3. Check `PICCData[0] == 0xc7`. 3. Check `PICCData[0] == 0xc7`.
4. Calculate `ID=PRF(IssuerKey, '2d003f7b' || UID)` with the `UID` from the `PICCData`. 4. Calculate `ID` with the `UID` from the `PICCData`.
5. Fetch `CardKey` from database with `ID`. 5. Fetch the BoltCard's `Version` with `ID` from the database.
6. Derive `K0`, `K2`, `K3`, `K4` with `CardKey` and the `UID`. 6. Ensure the BoltCard's state is `Configured`.
7. Verify that the SUN MAC in `c=` matches the one calculated using `Authentication Key (K2)`. 7. Generate `CardKey` with `UID` and `Version`.
8. Execute `AuthenticateEV2First` with `K0` 8. Derive `K0`, `K2`, `K3`, `K4` with `CardKey` and the `UID`.
9. Erase the NDEF data file using `WriteData` or `ISOUpdateBinary` 9. Verify that the SUN MAC in `c=` matches the one calculated using `Authentication Key (K2)`.
10. Restore the NDEF file settings to default values with `ChangeFileSettings`. 10. Execute `AuthenticateEV2First` with `K0`
11. Use `ChangeKey` with the recovered application keys to reset `K4` through `K0` to `00000000000000000000000000000000`. 11. Erase the NDEF data file using `WriteData` or `ISOUpdateBinary`
12. Restore the NDEF file settings to default values with `ChangeFileSettings`.
13. Use `ChangeKey` with the recovered application keys to reset `K4` through `K0` to `00000000000000000000000000000000`.
14. Update the BoltCard's state to `Reset` in the database.
Rational: Attempting to call `AuthenticateEV2First` without validating the `p=` and `c=` parameters could render the NTag inoperable after a few attempts. Rational: Attempting to call `AuthenticateEV2First` without validating the `p=` and `c=` parameters could render the NTag inoperable after a few attempts.
@ -66,12 +78,14 @@ If a `LNUrl Withdraw Service` needs to verify a payment request, follow these st
1. Read the NDEF lnurlw URL, extract `p=` and `c=`. 1. Read the NDEF lnurlw URL, extract `p=` and `c=`.
2. Derive `Encryption Key (K1)`, decrypts `p=` to get the `PICCData`. 2. Derive `Encryption Key (K1)`, decrypts `p=` to get the `PICCData`.
3. Check `PICCData[0] == 0xc7`. 3. Check `PICCData[0] == 0xc7`.
4. Calculate `ID=PRF(IssuerKey, '2d003f7b' || UID)` with the `UID` from the `PICCData`. 4. Calculate `ID` with the `UID` from the `PICCData`.
5. Fetch `CardKey` from database with `ID`. 5. Fetch the BoltCard's `Version` with `ID` from the database.
6. Derive `Authentication Key (K2)` with `CardKey` and the `UID`. 6. Ensure the BoltCard's state in the database is not `Reset`.
7. Verify that the SUN MAC in `c=` matches the one calculated using `Authentication Key (K2)`. 7. Generate `CardKey` with `UID` and `Version`.
8. Confirm that the last-seen counter for `ID` is lower than what is stored in `counter=PICCData[8..11]`. (Little Endian) 8. Derive `Authentication Key (K2)` with `CardKey` and the `UID`.
9. Update the last-seen counter. 9. Verify that the SUN MAC in `c=` matches the one calculated using `Authentication Key (K2)`.
10. Confirm that the last-seen counter for `ID` is lower than what is stored in `counter=PICCData[8..11]`. (Little Endian)
11. Update the last-seen counter.
Rationale: The `ID` is calculated to prevent the exposure of the `UID` in the `LNUrl Withdraw Service` database. This approach provides both privacy and security. Specifically, because the `UID` is used to derive keys, it is preferable not to store it outside the NTag. Rationale: The `ID` is calculated to prevent the exposure of the `UID` in the `LNUrl Withdraw Service` database. This approach provides both privacy and security. Specifically, because the `UID` is used to derive keys, it is preferable not to store it outside the NTag.
@ -79,7 +93,7 @@ Rationale: The `ID` is calculated to prevent the exposure of the `UID` in the `L
A single `LNUrl Withdraw Service` can own multiple `IssuerKeys`. In such cases, it will need to attempt them all to decrypt `p=`, and pick the first one which satisfies `PICCData[0] == 0xc7` and verifies the `c=` checksum. A single `LNUrl Withdraw Service` can own multiple `IssuerKeys`. In such cases, it will need to attempt them all to decrypt `p=`, and pick the first one which satisfies `PICCData[0] == 0xc7` and verifies the `c=` checksum.
Using multiple `IssuerKeys`, can decrease the impact of a compromised `Encryption Key (K1)` at the cost of performance. Using multiple `IssuerKeys` can decrease the impact of a compromised `Encryption Key (K1)` at the cost of performance.
## Security consideration ## Security consideration
@ -92,7 +106,7 @@ Since `K1` is shared among multiple Bolt Cards, the security of this scheme is b
While NXP gives assurance keys can't be extracted, a non genuine NTag424 could potentially expose these keys. While NXP gives assurance keys can't be extracted, a non genuine NTag424 could potentially expose these keys.
Furthermore, because blank NTag424 uses the well-known initial application keys `00000000000000000000000000000000`, communication between the PCD and the PICC could be intercepted. If the Bolt Card setup doesn't occurs in a trusted environment, `K1` could be exposed during the calls to `ChangeKey`. Furthermore, because blank NTag424 uses the well-known initial application keys `00000000000000000000000000000000`, communication between the PCD and the PICC could be intercepted. If the BoltCard setup does not occur in a trusted environment, `K1` could be exposed during the calls to `ChangeKey`.
However, if `K1` is compromised, the attacker still cannot produce a valid checksum and can only recover the `UID` for tracking purposes. However, if `K1` is compromised, the attacker still cannot produce a valid checksum and can only recover the `UID` for tracking purposes.
@ -102,7 +116,7 @@ Note that verifying the signature returned by `Read_Sig` can only prove NXP issu
If the issuer's database is compromised, revealing both the IssuerKey and CardKeys, it would still be infeasible for an attacker to derive `K2` and thus to forge signatures for an arbitrary card. If the issuer's database is compromised, revealing both the IssuerKey and CardKeys, it would still be infeasible for an attacker to derive `K2` and thus to forge signatures for an arbitrary card.
This is because the database only stores `ID=PRF(IssuerKey, '2d003f7b' || UID)` and not the `UID` itself. This is because the database only stores `ID` and not the `UID` itself.
## Implementation notes {#notes} ## Implementation notes {#notes}
@ -179,7 +193,7 @@ static byte[] RotateLeft(byte[] b)
## Implementation ## Implementation
* [BTCPayServer.BoltCardTools](https://github.com/btcpayserver/BTCPayServer.BoltCardTools), a Boltcard/NTag424 library in C#. * [BTCPayServer.BoltCardTools](https://github.com/btcpayserver/BTCPayServer.BoltCardTools), a BoltCard/NTag424 library in C#.
## Test vectors ## Test vectors
@ -187,15 +201,16 @@ Input:
``` ```
UID: 04a39493cc8680 UID: 04a39493cc8680
Issuer Key: 00000000000000000000000000000001 Issuer Key: 00000000000000000000000000000001
Card Key: 00000000000000000000000000000002 Version: 1
``` ```
Expected: Expected:
``` ```
K0: 21940feffa2437910d8eb62b3b0a0648 K0: a29119fcb48e737d1591d3489557e49b
K1: 55da174c9608993dc27bb3f30a4a7314 K1: 55da174c9608993dc27bb3f30a4a7314
K2: 2934c4ab339979142dfd50ae0ca55dc2 K2: f4b404be700ab285e333e32348fa3d3b
K3: b696f18e5a79e5a0defb25c38109b8e3 K3: 73610ba4afe45b55319691cb9489142f
K4: c9d493b9d3e62ce963586aafcd7c6cfe K4: addd03e52964369be7f2967736b7bdb5
ID: e07ce1279d980ecb892a81924b67bf18 ID: e07ce1279d980ecb892a81924b67bf18
CardKey: ebff5a4e6da5ee14cbfe720ae06fbed9
``` ```