Simplify deterministic API

This commit is contained in:
nicolas.dorier 2023-10-22 21:37:39 +09:00
parent b0b2dd4a93
commit 789897fc14
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
9 changed files with 104 additions and 144 deletions

View File

@ -28,12 +28,13 @@ dotnet add package BTCPayServer.NTag424.PCSC
Then to use it:
```csharp
using BTCPayServer.NTag424;
using BTCPayServer.NTag424.PCSC;
using System;
using var ctx = PCSCContext.Create();
using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var key = new AESKey(new byte[16]);
var key = AESKey.Default;
await ntag.AuthenticateEV2First(0, key);
var id = await ntag.GetCardUID();
@ -46,9 +47,8 @@ Console.WriteLine($"Card UID: {idStr}");
```csharp
using BTCPayServer.NTag424.PCSC;
using System;
using NdefLibrary.Ndef;
using var ctx = PCSCContext.Create();
using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var uri = await ntag.TryReadNDefURI();
Console.WriteLine($"Card URI: {uri}");
@ -56,43 +56,27 @@ Console.WriteLine($"Card URI: {uri}");
### How to verify the signature of an NTag 424 smart card
BoltCards involve the cooperation of three types of agents:
* `Card Issuer`: This agent configures the cards for lightning payments. This includes setting up the card to use a specific `LNUrl Withdraw Service` and generating the access keys.
* `Payment processor`: This agent reads the card and forwards the payment request to the `LNUrl Withdraw Service`.
* `LNUrl Withdraw Service`: This service authenticates the card and completes the payment.
BoltCards setup involves three different type of access keys:
* The `IssuerKey`: Owned by the `Card Issuer`, this key is used to configure the card.
* The `EncryptionKey`: This key can either be unique to each card or shared among multiple cards. It must be known by the `LNUrl Withdraw Service`.
* The `AuthenticationKey`: This key should be unique and is used to authenticate the card. It must also be known by the `LNUrl Withdraw Service`.
If you are the `LNURL Withdraw Service`, here how to authenticate the card:
```csharp
using BTCPayServer.NTag424;
using BTCPayServer.NTag424.PCSC;
using System;
using System.Security;
using System.Collections;
using NdefLibrary.Ndef;
using System.Text.RegularExpressions;
// Set keys have you have setup the card
var encryptionKey = AESKey.Default;
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
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 piccData = PICCData.Create(encryptionKey.Decrypt(p));
// Note that the `piccData.Uid` contains the UID of the card which can be used to fetch
// the proper real `authenticationKey` of the card.
var authenticationKey = AESKey.Default;
if (!authenticationKey.CheckSunMac(c, piccData))
throw new Exception("Invalid card");
using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var uri = await ntag.TryReadNDefURI();
var p = Regex.Match(uri.AbsoluteUri, "p=(.*?)&").Groups[1].Value;
var c = Regex.Match(uri.AbsoluteUri, "c=(.*)").Groups[1].Value;
var piccData = PICCData.TryBoltcardDecrypt(encryptionKey, authenticationKey, p, c);
if (piccData == null)
throw new SecurityException("Impossible to decrypt or validate");
// The LNUrlw service should also check `piccData.Counter` is always increasing between payments to avoid replay attacks.
```
@ -105,7 +89,7 @@ using BTCPayServer.NTag424.PCSC;
using System;
using System.Collections;
using var ctx = PCSCContext.Create();
using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
// Example with hard coded keys
@ -127,34 +111,37 @@ await ntag.SetupBoltcard(lnurlwService, BoltcardKeys.Default, keys);
### How to setup a bolt card with deterministic keys, and decrypt the PICCData
[Deterministic keys](https://github.com/boltcard/boltcard/blob/main/docs/DETERMINISTIC.md) 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);`.
[Deterministic keys](https://github.com/boltcard/boltcard/blob/main/docs/DETERMINISTIC.md) simplifies the management of Boltcard by removing the need to store the keys of each Boltcards in a database.
Here is an example of how to setup a card with deterministic keys, and decrypt the PICCData.
```csharp
using BTCPayServer.NTag424;
using BTCPayServer.NTag424.PCSC;
using System;
using System.Security;
using System.Collections;
using System.Text.RegularExpressions;
using var ctx = PCSCContext.Create();
using var ctx = await PCSCContext.WaitForCard();
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 batchKeys = new DeterministicBatchKeys(issuerKey, BatchId: 0);
var piccData = PICCData.TryDeterministicBoltcardDecrypt(issuerKey, p, c, uid, batchId: 0);
var uri = await ntag.TryReadNDefURI();
var p = Regex.Match(uri.AbsoluteUri, "p=(.*?)&").Groups[1].Value;
var c = Regex.Match(uri.AbsoluteUri, "c=(.*)").Groups[1].Value;
var piccData = PICCData.TryDeterministicBoltcardDecrypt(batchKeys, p, c, uid);
if (piccData == null)
throw new SecurityException("Impossible to decrypt with issuerKey");
throw new SecurityException("Impossible to decrypt with batchKeys");
// 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);`.
// You can reset the card with `await ntag.ResetCard(batchKeys);`.
```
## License

View File

@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.8</Version>
<Version>1.0.9</Version>
</PropertyGroup>
<PropertyGroup>

View File

@ -41,7 +41,7 @@ public class AESKey
public byte[] Decrypt(string hex, byte[]? iv = null)
{
ArgumentNullException.ThrowIfNull(hex);
return Decrypt(hex.HexToBytes());
return Decrypt(hex.HexToBytes(), iv);
}
public byte[] Decrypt(ReadOnlySpan<byte> cypherText, byte[]? iv = null)
{
@ -223,77 +223,4 @@ public class AESKey
{
return new AESKey(RandomNumberGenerator.GetBytes(BLOCK_SIZE));
}
/// <summary>
/// Create a new boltcard encryption key from the issuer key and a batch id
/// K1 = 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 app master key from the issuer key and a uid
/// CMacDerive(encryptionKey, "2d003f76" + uid)
/// </summary>
/// <param name="batch">The UID</param>
/// <returns>The app master key for the batch of boltcard</returns>
public AESKey DeriveAppMasterKey(byte[] uid, uint batchId = 0)
{
return Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x76 },
UIntToBytesLE(batchId),
uid));
}
/// <summary>
/// Create a new boltcard k3 and k4 key from the issuer key and uid.
/// Those keys aren't used by the remark of ntag 424 cards indicates it should be set on the card.
/// K3=CMacDerive(encryptionKey, "2d003f79" + batchId + uid)
/// K4=CMacDerive(encryptionKey, "2d003f7a" + batchId + uid)
/// </summary>
/// <param name="uid">UID of the card</param>
/// <returns>The 0x03 and 0x04 keys</returns>
public (AESKey K3, AESKey K4) DeriveK3K4(byte[] uid, uint batchId = 0)
{
return (Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
UIntToBytesLE(batchId),
uid)),
Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
UIntToBytesLE(batchId),
uid)));
}
/// <summary>
/// Derive boltcard key K2 from the issuer key, used for authentication
/// </summary>
/// <param name="uid">UID of the card</param>
/// <returns>The K2 key</returns>
public AESKey DeriveAuthenticationKey(byte[] uid, uint batchId = 0)
{
return Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
UIntToBytesLE(batchId),
uid));
}
/// <summary>
/// Get the ID from the UID and the encryption key (K1)
/// </summary>
/// <param name="uid">The UID</param>
/// <returns>The ID</returns>
public byte[] GetId(byte[] uid, uint batchId = 0)
{
return Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7b },
UIntToBytesLE(batchId),
uid)).ToBytes().Take(7).ToArray();
}
}

View File

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<Version>1.0.8</Version>
<Version>1.0.9</Version>
</PropertyGroup>
<PropertyGroup>

View File

@ -22,13 +22,4 @@ 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 appMasterKey = issuerKey.DeriveAppMasterKey(uid, batchId);
var authKey = issuerKey.DeriveAuthenticationKey(uid, batchId);
var k = issuerKey.DeriveK3K4(uid, batchId);
return new BoltcardKeys(appMasterKey, encryptionKey, authKey, k.K3, k.K4);
}
}

View File

@ -0,0 +1,53 @@
using System.Linq;
using static BTCPayServer.NTag424.Helpers;
namespace BTCPayServer.NTag424;
public record DeterministicBatchKeys(AESKey IssuerKey, uint BatchId = 0)
{
public AESKey DeriveEncryptionKey()
{
return IssuerKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x77 },
UIntToBytesLE(BatchId)));
}
public AESKey DeriveAuthenticationKey(byte[] uid)
{
return IssuerKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
UIntToBytesLE(BatchId),
uid));
}
public BoltcardKeys DeriveBoltcardKeys(byte[] uid)
{
var appMasterKey = IssuerKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x76 },
UIntToBytesLE(BatchId),
uid));
var encryptionKey = DeriveEncryptionKey();
var authKey = DeriveAuthenticationKey(uid);
var k1 = IssuerKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
UIntToBytesLE(BatchId),
uid));
var k2 = IssuerKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
UIntToBytesLE(BatchId),
uid));
return new BoltcardKeys(appMasterKey, encryptionKey, authKey, k1, k2);
}
/// <summary>
/// Get the ID from the UID and the encryption key (K1)
/// </summary>
/// <param name="uid">The UID</param>
/// <returns>The ID</returns>
public byte[] GetId(byte[] uid)
{
return IssuerKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7b },
UIntToBytesLE(BatchId),
uid)).ToBytes().Take(7).ToArray();
}
}

View File

@ -396,15 +396,15 @@ public class Ntag424
/// <param name="issuerKey">The issuer key</param>
/// <param name="batchId">The batch id</param>
/// <returns></returns>
public async Task ResetCard(AESKey issuerKey, uint batchId = 0)
public async Task ResetCard(DeterministicBatchKeys batchKeys)
{
var encryptionKey = issuerKey.DeriveEncryptionKey(batchId);
var encryptionKey = batchKeys.DeriveEncryptionKey();
if (CurrentSession is null)
await AuthenticateEV2First(1, encryptionKey);
if (issuerKey != CurrentSession!.Key)
if (encryptionKey != CurrentSession!.Key)
await AuthenticateEV2NonFirst(1, encryptionKey);
var uid = await GetCardUID();
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId);
var keys = batchKeys.DeriveBoltcardKeys(uid);
await ResetCard(keys);
}

View File

@ -44,23 +44,22 @@ public record PICCData(byte[]? Uid, int? Counter)
/// <summary>
/// Decrypt the PICCData from the BoltCard and check the checksum.
/// It assumes deterministic keys are used.
/// </summary>
/// <param name="issuerKey">The issuer key to use for the derivation of K1 and K2</param>
/// <param name="batchKeys">The deterministic batch keys</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 or null.</returns>
public static BoltcardPICCData? TryDeterministicBoltcardDecrypt(AESKey issuerKey, string p, string c, byte[]? payload = null, uint batchId = 0)
public static BoltcardPICCData? TryDeterministicBoltcardDecrypt(DeterministicBatchKeys batchKeys, string p, string c, byte[]? payload = null)
{
if (!Validate(p, c))
return null;
var encryptionKey = issuerKey.DeriveEncryptionKey(batchId);
var encryptionKey = batchKeys.DeriveEncryptionKey();
var bytes = encryptionKey.Decrypt(p.HexToBytes());
if (bytes[0] != 0xc7)
return null;
var piccData = new BoltcardPICCData(Create(bytes));
var authenticationKey = issuerKey.DeriveAuthenticationKey(piccData.Uid, batchId);
var authenticationKey = batchKeys.DeriveAuthenticationKey(piccData.Uid);
if (!authenticationKey.CheckSunMac(c, piccData, payload))
return null;
return new BoltcardPICCData(piccData);

View File

@ -79,13 +79,14 @@ public class UnitTest1
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
var batchId = 1U;
var uid = "04a39493cc8680".HexToBytes();
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId);
var batchKeys = new DeterministicBatchKeys(issuerKey, batchId);
var keys = batchKeys.DeriveBoltcardKeys(uid);
Logs.WriteLine("K0: " + keys.AppMasterKey.ToBytes().ToHex());
Logs.WriteLine("K1: " + keys.EncryptionKey.ToBytes().ToHex());
Logs.WriteLine("K2: " + keys.AuthenticationKey.ToBytes().ToHex());
Logs.WriteLine("K3: " + keys.K3.ToBytes().ToHex());
Logs.WriteLine("K4: " + keys.K4.ToBytes().ToHex());
Logs.WriteLine("ID: " + keys.EncryptionKey.GetId(uid, batchId).ToHex());
Logs.WriteLine("ID: " + batchKeys.GetId(uid).ToHex());
}
[Fact]
@ -175,12 +176,13 @@ public class UnitTest1
public async Task Reset()
{
var issuerKey = new AESKey("01000000000000000000000000000000".HexToBytes());
var batchKeys = new DeterministicBatchKeys(issuerKey);
using var ctx = PCSCContext.Create();
var enc = issuerKey.DeriveEncryptionKey();
var enc = batchKeys.DeriveEncryptionKey();
var ntag = ctx.CreateNTag424();
await ntag.AuthenticateEV2First(1, enc);
var uid = await ntag.GetCardUID();
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid);
var keys = batchKeys.DeriveBoltcardKeys(uid);
await ntag.ResetCard(keys);
}
@ -239,7 +241,8 @@ public class UnitTest1
//await ntag.ResetCard(issuerKey);
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId: 0);
var batchKeys = new DeterministicBatchKeys(issuerKey);
var keys = batchKeys.DeriveBoltcardKeys(uid);
await ntag.SetupBoltcard("http://test.com", BoltcardKeys.Default, keys);
var message = await ntag.ReadNDef();
@ -247,9 +250,9 @@ public class UnitTest1
var p = Regex.Match(uri, "p=(.*?)&").Groups[1].Value;
var c = Regex.Match(uri, "c=(.*)").Groups[1].Value;
var piccData = PICCData.TryDeterministicBoltcardDecrypt(issuerKey, p, c, batchId: 0);
var piccData = PICCData.TryDeterministicBoltcardDecrypt(batchKeys, p, c);
Assert.NotNull(piccData);
await ntag.ResetCard(issuerKey);
await ntag.ResetCard(batchKeys);
}
[Fact]