Deterministic boltcards

This commit is contained in:
nicolas.dorier 2023-10-09 19:08:53 +09:00
parent 65d575849c
commit cff973b18a
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
6 changed files with 157 additions and 6 deletions

View File

@ -122,6 +122,38 @@ await ntag.SetupBoltcard(lnurlwService, BoltcardKeys.Default, keys);
// You can reset the card to its factory state with `await ntag.ResetCard(keys);`
```
### How to setup a bolt card with deterministic keys, and decrypt the PICCData
Deterministic keys are useful if you want to be able to recover the keys of the card from a seed.
* The issuer can recover the keys of any card, just with a batchId and the issuer key.
* The LNUrlw service can recover the keys of any card (except the issuer key), just with the encryption key.
Note that you can reset the card to its factory state by only knowing the `issuerKey` with `await ntag.ResetCard(issuerKey);`.
```csharp
using BTCPayServer.NTag424;
using BTCPayServer.NTag424.PCSC;
using System;
using System.Collections;
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId: 0);
var lnurlwService = "lnurlw://test.com";
var encryptionKey = keys.EncryptionKey;
var piccData = PICCData.BoltcardDecrypt(encryptionKey, p, c);
// If this method didn't throw an exception, it has been successfully decrypted and authenticated.
// You can reset the card with `await ntag.ResetCard(issuerKey);`.
```
## License
MIT

View File

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using static BTCPayServer.NTag424.Helpers;
@ -79,10 +80,6 @@ public class AESKey
var cmac = derived.CMac(payload);
return Truncate(cmac);
}
public PICCData DecryptSun(byte[] data)
{
return PICCData.Create(Decrypt(data));
}
AESKey SesSDMFileReadMACKey(byte[]? uid, int? counter)
{
int i = 0;
@ -221,4 +218,52 @@ public class AESKey
code.AddBytes(_bytes);
return code.ToHashCode();
}
public static AESKey Random()
{
return new AESKey(RandomNumberGenerator.GetBytes(BLOCK_SIZE));
}
/// <summary>
/// Create a new boltcard encryption key from the issuer key and a batch id
/// CMacDerive(encryptionKey, "2d003f77" + batchId) where batchId is LE encoded on 4 bytes.
/// </summary>
/// <param name="batch">Batch id</param>
/// <returns>The encryption key for the batch of boltcard</returns>
public AESKey DeriveEncryptionKey(uint batchId = 0)
{
return Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x77 },
UIntToBytesLE(batchId)));
}
/// <summary>
/// Create a new boltcard authentication key from the encryption key and uid.
/// CMacDerive(encryptionKey, "2d003f78" + uid)
/// </summary>
/// <param name="uid">UID of the card</param>
/// <returns>Authentication key of the card</returns>
public AESKey DeriveAuthenticationKey(byte[] uid)
{
return Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
uid));
}
/// <summary>
/// Create a new boltcard k3 and k4 key from the encryption key and uid.
/// Those keys aren't used by the remark of ntag 424 cards indicates it should be set on the card.
/// CMacDerive(encryptionKey, "2d003f79" + uid), CMacDerive(encryptionKey, "2d003f7a" + uid)
/// </summary>
/// <param name="uid">UID of the card</param>
/// <returns>The 0x03 and 0x04 keys</returns>
public (AESKey K1, AESKey K2) DeriveK3K4(byte[] uid)
{
return (Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
uid)),
Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
uid)));
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Security.Cryptography.X509Certificates;
namespace BTCPayServer.NTag424;
@ -21,4 +22,12 @@ public record BoltcardKeys(
public BoltcardKeys() : this (AESKey.Default, AESKey.Default, AESKey.Default, AESKey.Default, AESKey.Default)
{
}
public static BoltcardKeys CreateDeterministicKeys(AESKey issuerKey, byte[] uid, uint batchId = 0)
{
var encryptionKey = issuerKey.DeriveEncryptionKey(batchId);
var authenticationKey = encryptionKey.DeriveAuthenticationKey(uid);
(var k3, var k4) = encryptionKey.DeriveK3K4(uid);
return new BoltcardKeys(issuerKey, encryptionKey, authenticationKey, k3, k4);
}
}

View File

@ -375,6 +375,16 @@ public class Ntag424
CurrentSession = null;
}
public async Task ResetCard(AESKey issuerKey, uint batchId = 0)
{
if (CurrentSession is null)
await AuthenticateEV2First(0, issuerKey);
if (issuerKey != CurrentSession!.Key)
await AuthenticateEV2NonFirst(0, issuerKey);
var uid = await GetCardUID();
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId);
await ResetCard(keys);
}
public async Task ResetCard(BoltcardKeys keys)
{
if (CurrentSession is null)

View File

@ -1,5 +1,7 @@
using System;
using System.Runtime.Loader;
using System.Security;
namespace BTCPayServer.NTag424;
@ -25,4 +27,35 @@ public record PICCData(byte[]? Uid, int? Counter)
}
return new PICCData(uid, counter);
}
/// <summary>
/// Decrypt the PICCData from the BoltCard and check the checksum.
/// It assumes the authentication key has been derived from the encryption key. (ie. CMacDerive(encryptionKey, '2d003f78' + uid))
/// </summary>
/// <param name="encryptionKey">The encryption key for p</param>
/// <param name="p">The p= parameter from the lnurlw (encrypted PICCData)</param>
/// <param name="c">THe c= parameter from the lnurlw (checksum)</param>
/// <param name="payload">Optional payload committed by c</param>
/// <returns>The PICCData if the checksum passed verification.</returns>
/// <exception cref="SecurityException">Invalid PICCData or checksum</exception>
public static PICCData BoltcardDecrypt(AESKey encryptionKey, string p, string c, byte[]? payload = null)
{
var bytes = encryptionKey.Decrypt(p.HexToBytes());
PICCData piccData;
try
{
piccData = PICCData.Create(bytes);
}
catch
{
throw new SecurityException("Invalid PICCData");
}
if (piccData.Uid is null)
throw new SecurityException("No UID found in the PICCData");
var authenticationKey = encryptionKey.DeriveAuthenticationKey(piccData.Uid);
if (!authenticationKey.CheckSunMac(c, piccData, payload))
throw new SecurityException("Incorrect checksum for the PICCDAta");
return piccData;
}
}

View File

@ -51,7 +51,7 @@ public class UnitTest1
public void CanDecryptSunPICCData(string encrypted, string uid, int ctr)
{
var key = new AESKey(Convert.FromHexString("0c3b25d92b38ae443229dd59ad34b85d"));
var picc = key.DecryptSun(encrypted.HexToBytes());
var picc = PICCData.Create(key.Decrypt(encrypted.HexToBytes()));
Assert.Equal(ctr, picc.Counter);
Assert.Equal(uid.ToLowerInvariant(), picc.Uid?.ToHex());
}
@ -172,7 +172,6 @@ public class UnitTest1
{
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
var initialKey = AESKey.Default;
var keys = new BoltcardKeys(
IssuerKey: new AESKey("00000000000000000000000000000001".HexToBytes()),
EncryptionKey: new AESKey("00000000000000000000000000000002".HexToBytes()),
@ -190,6 +189,29 @@ public class UnitTest1
await ntag.ResetCard(keys);
}
[Fact]
public async Task CanDoDeterministicBoltcard()
{
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
// await ntag.ResetCard(issuerKey);
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId: 0);
// await ntag.ResetCard(keys);
await ntag.SetupBoltcard("http://test.com", BoltcardKeys.Default, keys);
var message = await ntag.ReadNDef();
var uri = new NdefUriRecord(message[0]).Uri;
var p = Regex.Match(uri, "p=(.*?)&").Groups[1].Value;
var c = Regex.Match(uri, "c=(.*)").Groups[1].Value;
var encryptionKey = keys.EncryptionKey;
var piccData = PICCData.BoltcardDecrypt(encryptionKey, p, c);
await ntag.ResetCard(issuerKey);
}
[Fact]
public void CanCalculateCRC()
{