Simplify deterministic API
This commit is contained in:
parent
b0b2dd4a93
commit
789897fc14
73
README.md
73
README.md
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
53
src/BTCPayServer.NTag424/DeterministicBatchKeys.cs
Normal file
53
src/BTCPayServer.NTag424/DeterministicBatchKeys.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user