10 KiB
Abstract
The NXP NTAG424DNA allows applications to configure five application keys, named K0, K1, K2, K3, and K4. In the BoltCard configuration:
K0is theApp Master Key, it is the only key permitted to change the application keys.K1serves as theencryption keyfor thePICCData, represented by thep=parameter.K2is theauthentication keyused for calculating the SUN MAC of thePICCData, represented by thec=parameter.K3andK4are not used but should be configured as recommended in the NTag424 application notes.
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.
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.
Keys generation
First, the LNUrl Withdraw Service generates a IssuerKey that it will use to generate the keys for every NTag424.
Then, configure a BoltCard as follows:
CardKey = PRF(IssuerKey, '2d003f75' || UID || Version)K0 = PRF(CardKey, '2d003f76')K1 = PRF(IssuerKey, '2d003f77')K2 = PRF(CardKey, '2d003f78')K3 = PRF(CardKey, '2d003f79')K4 = PRF(CardKey, '2d003f7a')ID = PRF(IssuerKey, '2d003f7b' || UID)
With the following parameters:
IssuerKey: This 16-bytes key is used by anLNUrl Withdraw Serviceto setup all its BoltCards.UID: This is the 7-byte ID of the card. You can retrieve it from the NTag424 using theGetCardUIDfunction after identification with K1, or by decrypting thep=parameter, also known asPICCData.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 sameLNUrl 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
How to setup a new BoltCard
- Execute
ReadDataorISOReaDBinaryon the BoltCard to ensure the card is blank. - Execute
AuthenticateEV2Firstwith the application key00000000000000000000000000000000 - Fetch the
UIDwithGetCardUID. - Calculate
ID - Fetch the
StateandVersionof the BoltCard with the specifiedIDfrom the database. - Ensure either:
- If no BoltCard is found, insert an entry in the database with
Version=0and its state set toConfigured. - If a BoltCard is found and its state is
Resetthen incrementVersionby1, and change its state toConfigured.
- If no BoltCard is found, insert an entry in the database with
- Generate
CardKeywithUIDandVersion. - Calculate
K0,K1,K2,K3,K4. - Setup the BoltCard.
How to implement a Reset feature
If a LNUrl Withdraw Service offers a factory reset feature for a user's BoltCard, here is the recommended procedure:
- Read the NDEF lnurlw URL, extract
p=andc=. - Derive
Encryption Key (K1), decryptp=to obtain thePICCData. - Check
PICCData[0] == 0xc7. - Calculate
IDwith theUIDfrom thePICCData. - Fetch the BoltCard's
VersionwithIDfrom the database. - Ensure the BoltCard's state is
Configured. - Generate
CardKeywithUIDandVersion. - Derive
K0,K2,K3,K4withCardKeyand theUID. - Verify that the SUN MAC in
c=matches the one calculated usingAuthentication Key (K2). - Execute
AuthenticateEV2FirstwithK0 - Erase the NDEF data file using
WriteDataorISOUpdateBinary - Restore the NDEF file settings to default values with
ChangeFileSettings. - Use
ChangeKeywith the recovered application keys to resetK4throughK0to00000000000000000000000000000000. - Update the BoltCard's state to
Resetin the database.
Rational: Attempting to call AuthenticateEV2First without validating the p= and c= parameters could render the NTag inoperable after a few attempts.
How to implement a verification
If a LNUrl Withdraw Service needs to verify a payment request, follow these steps:
- Read the NDEF lnurlw URL, extract
p=andc=. - Derive
Encryption Key (K1), decryptsp=to get thePICCData. - Check
PICCData[0] == 0xc7. - Calculate
IDwith theUIDfrom thePICCData. - Fetch the BoltCard's
VersionwithIDfrom the database. - Ensure the BoltCard's state in the database is not
Reset. - Generate
CardKeywithUIDandVersion. - Derive
Authentication Key (K2)withCardKeyand theUID. - Verify that the SUN MAC in
c=matches the one calculated usingAuthentication Key (K2). - Confirm that the last-seen counter for
IDis lower than what is stored incounter=PICCData[8..11]. (Little Endian) - 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.
Multiple IssuerKeys
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.
Security consideration
K1 security
Since K1 is shared among multiple BoltCards, the security of this scheme is based on the following assumptions:
K1cannot be extracted from a legitimate NTag424.- BoltCard setup occurs in a trusted environment.
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 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.
Note that verifying the signature returned by Read_Sig can only prove NXP issued a card with a specific UID. It cannot prove that the current communication channel is established with an authentic NTag424. This is because the signature returned by Read_Sig covers only the UID and can therefore be replayed by a non-genuine NTag424.
Issuer database security
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 and not the UID itself.
Implementation notes
Here is a C# implementation of the CMAC algorithm described in NIST Special Publication 800-38B.
public byte[] CMac(byte[] data)
{
var key = _bytes;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] L = AesEncrypt(key, new byte[16], new byte[16]);
// step 2, K1 is derived through the following operation:
byte[]
FirstSubkey =
RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
if ((L[0] & 0x80) == 0x80)
FirstSubkey[15] ^=
0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit.
// step 3, K2 is derived through the following operation:
byte[]
SecondSubkey =
RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
if ((FirstSubkey[0] & 0x80) == 0x80)
SecondSubkey[15] ^=
0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit.
// MAC computing
if (((data.Length != 0) && (data.Length % 16 == 0)) == true)
{
// If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < FirstSubkey.Length; j++)
data[data.Length - 16 + j] ^= FirstSubkey[j];
}
else
{
// Otherwise, the last block shall be padded with 10^i
byte[] padding = new byte[16 - data.Length % 16];
padding[0] = 0x80;
data = data.Concat(padding.AsEnumerable()).ToArray();
// and exclusive-OR'ed with K2
for (int j = 0; j < SecondSubkey.Length; j++)
data[data.Length - 16 + j] ^= SecondSubkey[j];
}
// The result of the previous process will be the input of the last encryption.
byte[] encResult = AesEncrypt(key, new byte[16], data);
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
return HashValue;
}
static byte[] RotateLeft(byte[] b)
{
byte[] r = new byte[b.Length];
byte carry = 0;
for (int i = b.Length - 1; i >= 0; i--)
{
ushort u = (ushort)(b[i] << 1);
r[i] = (byte)((u & 0xff) + carry);
carry = (byte)((u & 0xff00) >> 8);
}
return r;
}
Implementation
- BTCPayServer.BoltCardTools, a BoltCard/NTag424 library in C#.
Test vectors
Input:
UID: 04a39493cc8680
Issuer Key: 00000000000000000000000000000001
Version: 1
Expected:
K0: a29119fcb48e737d1591d3489557e49b
K1: 55da174c9608993dc27bb3f30a4a7314
K2: f4b404be700ab285e333e32348fa3d3b
K3: 73610ba4afe45b55319691cb9489142f
K4: addd03e52964369be7f2967736b7bdb5
ID: e07ce1279d980ecb892a81924b67bf18
CardKey: ebff5a4e6da5ee14cbfe720ae06fbed9