From 789897fc1452698e4e20c99e2f24f5b5353be251 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 22 Oct 2023 21:37:39 +0900 Subject: [PATCH] Simplify deterministic API --- README.md | 73 ++++++++---------- .../BTCPayServer.NTag424.PCSC.csproj | 2 +- src/BTCPayServer.NTag424/AESKey.cs | 75 +------------------ .../BTCPayServer.NTag424.csproj | 2 +- src/BTCPayServer.NTag424/BoltcardKeys.cs | 9 --- .../DeterministicBatchKeys.cs | 53 +++++++++++++ src/BTCPayServer.NTag424/Ntag424.cs | 8 +- src/BTCPayServer.NTag424/PICCData.cs | 9 +-- tests/UnitTest1.cs | 17 +++-- 9 files changed, 104 insertions(+), 144 deletions(-) create mode 100644 src/BTCPayServer.NTag424/DeterministicBatchKeys.cs diff --git a/README.md b/README.md index 743cae2..39b6cab 100644 --- a/README.md +++ b/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 diff --git a/src/BTCPayServer.NTag424.PCSC/BTCPayServer.NTag424.PCSC.csproj b/src/BTCPayServer.NTag424.PCSC/BTCPayServer.NTag424.PCSC.csproj index 848ea7b..ceef971 100644 --- a/src/BTCPayServer.NTag424.PCSC/BTCPayServer.NTag424.PCSC.csproj +++ b/src/BTCPayServer.NTag424.PCSC/BTCPayServer.NTag424.PCSC.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 1.0.8 + 1.0.9 diff --git a/src/BTCPayServer.NTag424/AESKey.cs b/src/BTCPayServer.NTag424/AESKey.cs index 9c56c48..6c16bb0 100644 --- a/src/BTCPayServer.NTag424/AESKey.cs +++ b/src/BTCPayServer.NTag424/AESKey.cs @@ -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 cypherText, byte[]? iv = null) { @@ -223,77 +223,4 @@ public class AESKey { return new AESKey(RandomNumberGenerator.GetBytes(BLOCK_SIZE)); } - - /// - /// 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. - /// - /// Batch id - /// The encryption key for the batch of boltcard - public AESKey DeriveEncryptionKey(uint batchId = 0) - { - return Derive(Helpers.Concat( - new byte[] { 0x2d, 0x00, 0x3f, 0x77 }, - UIntToBytesLE(batchId))); - } - - /// - /// Create a new boltcard app master key from the issuer key and a uid - /// CMacDerive(encryptionKey, "2d003f76" + uid) - /// - /// The UID - /// The app master key for the batch of boltcard - public AESKey DeriveAppMasterKey(byte[] uid, uint batchId = 0) - { - return Derive(Helpers.Concat( - new byte[] { 0x2d, 0x00, 0x3f, 0x76 }, - UIntToBytesLE(batchId), - uid)); - } - - /// - /// 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) - /// - /// UID of the card - /// The 0x03 and 0x04 keys - 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))); - } - - /// - /// Derive boltcard key K2 from the issuer key, used for authentication - /// - /// UID of the card - /// The K2 key - public AESKey DeriveAuthenticationKey(byte[] uid, uint batchId = 0) - { - return Derive(Helpers.Concat( - new byte[] { 0x2d, 0x00, 0x3f, 0x78 }, - UIntToBytesLE(batchId), - uid)); - } - - /// - /// Get the ID from the UID and the encryption key (K1) - /// - /// The UID - /// The ID - 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(); - } } diff --git a/src/BTCPayServer.NTag424/BTCPayServer.NTag424.csproj b/src/BTCPayServer.NTag424/BTCPayServer.NTag424.csproj index c1bf8fd..409fc69 100644 --- a/src/BTCPayServer.NTag424/BTCPayServer.NTag424.csproj +++ b/src/BTCPayServer.NTag424/BTCPayServer.NTag424.csproj @@ -5,7 +5,7 @@ net6.0 10.0 enable - 1.0.8 + 1.0.9 diff --git a/src/BTCPayServer.NTag424/BoltcardKeys.cs b/src/BTCPayServer.NTag424/BoltcardKeys.cs index 3f79a37..4e87901 100644 --- a/src/BTCPayServer.NTag424/BoltcardKeys.cs +++ b/src/BTCPayServer.NTag424/BoltcardKeys.cs @@ -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); - } } diff --git a/src/BTCPayServer.NTag424/DeterministicBatchKeys.cs b/src/BTCPayServer.NTag424/DeterministicBatchKeys.cs new file mode 100644 index 0000000..a85eacc --- /dev/null +++ b/src/BTCPayServer.NTag424/DeterministicBatchKeys.cs @@ -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); + } + + /// + /// Get the ID from the UID and the encryption key (K1) + /// + /// The UID + /// The ID + public byte[] GetId(byte[] uid) + { + return IssuerKey.Derive(Helpers.Concat( + new byte[] { 0x2d, 0x00, 0x3f, 0x7b }, + UIntToBytesLE(BatchId), + uid)).ToBytes().Take(7).ToArray(); + } +} diff --git a/src/BTCPayServer.NTag424/Ntag424.cs b/src/BTCPayServer.NTag424/Ntag424.cs index 8ba9ee9..87420fc 100644 --- a/src/BTCPayServer.NTag424/Ntag424.cs +++ b/src/BTCPayServer.NTag424/Ntag424.cs @@ -396,15 +396,15 @@ public class Ntag424 /// The issuer key /// The batch id /// - 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); } diff --git a/src/BTCPayServer.NTag424/PICCData.cs b/src/BTCPayServer.NTag424/PICCData.cs index c85070c..e770e44 100644 --- a/src/BTCPayServer.NTag424/PICCData.cs +++ b/src/BTCPayServer.NTag424/PICCData.cs @@ -44,23 +44,22 @@ public record PICCData(byte[]? Uid, int? Counter) /// /// Decrypt the PICCData from the BoltCard and check the checksum. - /// It assumes deterministic keys are used. /// - /// The issuer key to use for the derivation of K1 and K2 + /// The deterministic batch keys /// The p= parameter from the lnurlw (encrypted PICCData) /// The c= parameter from the lnurlw (checksum) /// Optional payload committed by c /// The PICCData if the checksum passed verification or null. - 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); diff --git a/tests/UnitTest1.cs b/tests/UnitTest1.cs index f10b886..562ca0d 100644 --- a/tests/UnitTest1.cs +++ b/tests/UnitTest1.cs @@ -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]