Change spec deterministic

This commit is contained in:
nicolas.dorier 2023-10-24 15:37:23 +09:00
parent a812f6fdd2
commit 339d0852d5
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
9 changed files with 265 additions and 144 deletions

View File

@ -71,7 +71,7 @@ using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var uri = await ntag.TryReadNDefURI();
var piccData = PICCData.TryBoltcardDecrypt(encryptionKey, authenticationKey, uri);
var piccData = PICCData.TryBoltcardDecryptCheck(encryptionKey, authenticationKey, uri);
if (piccData == null)
throw new SecurityException("Impossible to decrypt or validate");
@ -112,28 +112,36 @@ await ntag.SetupBoltcard(lnurlwService, BoltcardKeys.Default, keys);
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 var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
// First time authenticate is with the default 00.000 key
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
var batchKeys = new DeterministicBatchKeys(issuerKey, BatchId: 0);
//var nonce = IssuerKey.RandomNonce();
var nonce = new byte[16]; // Please use IssuerKey.RandomNonce() in production
// SaveNonce should be implemented by the server
await SaveNonce(issuerKey.GetId(uid), nonce);
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
await ntag.SetupBoltcard("lnurlw://blahblah.com", BoltcardKeys.Default, keys);
var uri = await ntag.TryReadNDefURI();
var piccData = PICCData.TryDeterministicBoltcardDecrypt(batchKeys, uri, uid);
var piccData = issuerKey.TryDecrypt(uri);
if (piccData == null)
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(batchKeys);`.
// In real life, you would fetch the nonce from database
// var nonce = await FetchNonce(issuerKey.GetId(piccData.Uid));
if (!issuerKey.CheckSunMac(uri, piccData, nonce))
throw new SecurityException("Impossible to check the SUN MAC");
// 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, nonce);`.
```
## License

View File

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

View File

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using static BTCPayServer.NTag424.Helpers;
namespace BTCPayServer.NTag424;
@ -66,6 +67,8 @@ public class AESKey
}
public bool CheckSunMac(string mac, PICCData piccData, byte[]? payload = null)
{
if (mac is null || !Regex.IsMatch(mac, "[a-f0-9A-F]{16}"))
return false;
return CheckSunMac(mac.HexToBytes(), piccData, payload);
}

View File

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

View File

@ -1,53 +0,0 @@
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

@ -0,0 +1,121 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using static BTCPayServer.NTag424.Helpers;
namespace BTCPayServer.NTag424;
/// <summary>
/// Implement deterministic key derivation. <see cref="https://github.com/boltcard/boltcard/blob/main/docs/DETERMINISTIC.md"/>
/// </summary>
/// <param name="AESKey"></param>
public record IssuerKey(AESKey AESKey)
{
public IssuerKey(ReadOnlySpan<byte> bytes) : this(new AESKey(bytes))
{
}
public static byte[] RandomNonce()
{
var nonce = new byte[16];
RandomNumberGenerator.Fill(nonce);
return nonce;
}
public AESKey DeriveEncryptionKey()
{
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x77 }));
}
private void Validate(byte[] uid)
{
ArgumentNullException.ThrowIfNull(uid);
if (uid.Length != 7)
throw new ArgumentException("uid should be 7 bytes", nameof(uid));
}
private void Validate(byte[] uid, byte[] nonce)
{
Validate(uid);
ArgumentNullException.ThrowIfNull(nonce);
if (nonce.Length != 16)
throw new ArgumentException("nonce should be 16 bytes", nameof(nonce));
}
public AESKey DeriveAuthenticationKey(byte[] uid, byte[] nonce)
{
Validate(uid, nonce);
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
uid,
nonce));
}
public BoltcardKeys DeriveBoltcardKeys(byte[] uid, byte[] nonce)
{
Validate(uid, nonce);
var appMasterKey = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x76 },
uid,
nonce));
var encryptionKey = DeriveEncryptionKey();
var authKey = DeriveAuthenticationKey(uid, nonce);
var k1 = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
uid,
nonce));
var k2 = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
uid,
nonce));
return new BoltcardKeys(appMasterKey, encryptionKey, authKey, k1, k2);
}
/// <summary>
/// Get the ID from the UID
/// </summary>
/// <param name="uid">The UID</param>
/// <returns>The ID</returns>
public byte[] GetId(byte[] uid)
{
Validate(uid);
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7b },
uid)).ToBytes().ToArray();
}
/// <summary>
/// Decrypt the PICCData from the BoltCard and check the checksum.
/// </summary>
/// <param name="uri">The url with p= and c= parameters</param
/// <param name="payload">Optional payload committed by c</param>
/// <returns>The PICCData if the checksum passed verification or null.</returns>
public BoltcardPICCData? TryDecrypt(Uri? uri, byte[]? payload = null)
{
return BoltcardPICCData.TryDecrypt(DeriveEncryptionKey(), uri, payload);
}
/// <summary>
/// Decrypt the PICCData from the Boltcard and check the checksum.
/// </summary>
/// <param name="encryptionKey">The encryption key (K1)</param>
/// <param name="p">p= encrypted PICCData parameter</param>
/// <param name="c">c= checksum parameter</param>
/// <param name="payload">Optional payload committed by c</param>
/// <returns>The PICCData if the checksum passed verification or null.</returns>
public BoltcardPICCData? TryDecrypt(string p, string c, byte[]? payload = null)
{
return BoltcardPICCData.TryDecrypt(DeriveEncryptionKey(), p, c, payload);
}
public bool CheckSunMac([NotNullWhen(true)] Uri? uri, BoltcardPICCData piccData, byte[] nonce)
{
if (!PICCData.ExtractPC(uri, out _, out var c))
return false;
return this.DeriveAuthenticationKey(piccData.Uid, nonce).CheckSunMac(c, piccData);
}
public bool CheckSunMac([NotNullWhen(true)] string? c, BoltcardPICCData piccData, byte[] nonce)
{
return this.DeriveAuthenticationKey(piccData.Uid, nonce).CheckSunMac(c, piccData);
}
}

View File

@ -395,16 +395,17 @@ public class Ntag424
/// Reset the card to factory settings using current application keys using deterministic keys
/// </summary>
/// <param name="issuerKey">The issuer key</param>
/// <param name="nonce"></param>
/// <returns></returns>
public async Task ResetCard(DeterministicBatchKeys batchKeys)
public async Task ResetCard(IssuerKey issuerKey, byte[] nonce)
{
var encryptionKey = batchKeys.DeriveEncryptionKey();
var encryptionKey = issuerKey.DeriveEncryptionKey();
if (CurrentSession is null)
await AuthenticateEV2First(1, encryptionKey);
if (encryptionKey != CurrentSession!.Key)
await AuthenticateEV2NonFirst(1, encryptionKey);
var uid = await GetCardUID();
var keys = batchKeys.DeriveBoltcardKeys(uid);
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
await ResetCard(keys);
}

View File

@ -20,6 +20,49 @@ public record BoltcardPICCData : PICCData
public BoltcardPICCData(PICCData piccData) : this(piccData.Uid!, piccData.Counter!.Value)
{
}
/// <summary>
/// Decrypt the PICCData from the BoltCard and check the checksum.
/// </summary>
/// <param name="encryptionKey">The encryption key (K1)</param>
/// <param name="uri">The url with p= and c= parameters</param
/// <param name="payload">Optional payload committed by c</param>
/// <returns>The PICCData if the checksum passed verification or null.</returns>
public static BoltcardPICCData? TryDecrypt(AESKey encryptionKey, Uri? uri, byte[]? payload = null)
{
if (!ExtractPC(uri, out var p, out var c))
return null;
return TryDecrypt(encryptionKey, p, c, payload);
}
// PICCData for boltcard starts with 0xc7, and end with 5 bytes of 0
static bool ValidateBoltcardPICCData(byte[] piccData)
{
if (piccData is null || piccData.Length != 16)
return false;
return piccData[0] == 0xc7;
}
/// <summary>
/// Decrypt the PICCData from the Boltcard and check the checksum.
/// </summary>
/// <param name="encryptionKey">The encryption key (K1)</param>
/// <param name="p">p= encrypted PICCData parameter</param>
/// <param name="c">c= checksum parameter</param>
/// <param name="payload">Optional payload committed by c</param>
/// <returns>The PICCData if the checksum passed verification or null.</returns>
public static BoltcardPICCData? TryDecrypt(AESKey encryptionKey, string p, string c, byte[]? payload = null)
{
if (!Validate(p, c))
return null;
var bytes = encryptionKey.Decrypt(p.HexToBytes());
if (!ValidateBoltcardPICCData(bytes))
return null;
return new BoltcardPICCData(PICCData.Create(bytes));
}
}
public record PICCData(byte[]? Uid, int? Counter)
{
@ -44,56 +87,7 @@ public record PICCData(byte[]? Uid, int? Counter)
return new PICCData(uid, counter);
}
/// <summary>
/// Decrypt the PICCData from the BoltCard and check the checksum.
/// </summary>
/// <param name="batchKeys">The deterministic batch keys</param>
/// <param name="uri">The url with p= and c= parameters</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(DeterministicBatchKeys batchKeys, Uri? uri, byte[]? payload = null)
{
if (!ExtractPC(uri, out var p, out var c))
return null;
return TryDeterministicBoltcardDecrypt(batchKeys, p, c, payload);
}
/// <summary>
/// Decrypt the PICCData from the BoltCard and check the checksum.
/// </summary>
/// <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(DeterministicBatchKeys batchKeys, string p, string c, byte[]? payload = null)
{
if (!Validate(p, c))
return null;
var encryptionKey = batchKeys.DeriveEncryptionKey();
var bytes = encryptionKey.Decrypt(p.HexToBytes());
if (bytes[0] != 0xc7)
return null;
var piccData = new BoltcardPICCData(Create(bytes));
var authenticationKey = batchKeys.DeriveAuthenticationKey(piccData.Uid);
if (!authenticationKey.CheckSunMac(c, piccData, payload))
return null;
return new BoltcardPICCData(piccData);
}
private static bool Validate(string p, string c)
{
if (p.Length != 32 || c.Length != 16)
return false;
foreach(var ch in p.Concat(c))
{
if (Extensions.IsDigitCore(ch) == 0xff)
return false;
}
return true;
}
static bool ExtractPC(Uri? uri, [MaybeNullWhen(false)] out string p, [MaybeNullWhen(false)] out string c)
internal static bool ExtractPC(Uri? uri, [MaybeNullWhen(false)] out string p, [MaybeNullWhen(false)] out string c)
{
p = null;
c = null;
@ -103,8 +97,8 @@ public record PICCData(byte[]? Uid, int? Counter)
if (queryStringIdx == -1)
return false;
var queryString = uri.AbsoluteUri.Substring(queryStringIdx);
var pm = Regex.Match(queryString, "p=(.*?)&");
var cm = Regex.Match(queryString, "c=(.*)");
var pm = Regex.Match(queryString, "p=([a-f0-9A-F]{32})");
var cm = Regex.Match(queryString, "c=([a-f0-9A-F]{16})");
if (pm is null || cm is null)
return false;
p = pm.Groups[1].Value;
@ -120,11 +114,23 @@ public record PICCData(byte[]? Uid, int? Counter)
/// <param name="uri">The url with p= and c= parameters</param
/// <param name="payload">Optional payload committed by c</param>
/// <returns>The PICCData if the checksum passed verification or null.</returns>
public static PICCData? TryBoltcardDecrypt(AESKey encryptionKey, AESKey authenticationKey, Uri? uri, byte[]? payload = null)
public static PICCData? TryBoltcardDecryptCheck(AESKey encryptionKey, AESKey authenticationKey, Uri? uri, byte[]? payload = null)
{
if (!ExtractPC(uri, out var p, out var c))
return null;
return TryBoltcardDecrypt(encryptionKey, authenticationKey, p, c, payload);
return TryBoltcardDecryptCheck(encryptionKey, authenticationKey, p, c, payload);
}
protected static bool Validate(string p, string c)
{
if (p.Length != 32 || c.Length != 16)
return false;
foreach (var ch in p.Concat(c))
{
if (Extensions.IsDigitCore(ch) == 0xff)
return false;
}
return true;
}
/// <summary>
@ -136,7 +142,7 @@ public record PICCData(byte[]? Uid, int? Counter)
/// <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 PICCData? TryBoltcardDecrypt(AESKey encryptionKey, AESKey authenticationKey, string p, string c, byte[]? payload = null)
public static PICCData? TryBoltcardDecryptCheck(AESKey encryptionKey, AESKey authenticationKey, string p, string c, byte[]? payload = null)
{
if (!Validate(p, c))
return null;

View File

@ -1,4 +1,5 @@
using System.Collections;
using System.Security;
using System.Text.RegularExpressions;
using BTCPayServer.NTag424.PCSC;
using NdefLibrary.Ndef;
@ -76,17 +77,16 @@ public class UnitTest1
[Fact]
public void CanDeriveDeterministicBoltcard()
{
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
var batchId = 1U;
var uid = "04a39493cc8680".HexToBytes();
var batchKeys = new DeterministicBatchKeys(issuerKey, batchId);
var keys = batchKeys.DeriveBoltcardKeys(uid);
var nonce = "b45775776cb224c75bcde7ca3704e933".HexToBytes();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
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: " + batchKeys.GetId(uid).ToHex());
Logs.WriteLine("ID: " + issuerKey.GetId(uid).ToHex());
}
[Fact]
@ -172,18 +172,52 @@ public class UnitTest1
Assert.Equal(uid1.ToHex(), uid2.ToHex());
}
[Fact]
public async Task CanDecryptAndCheckSUNMacOnDeterministicKey()
{
using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
// First time authenticate is with the default 00.000 key
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var nonce = "00010000000000000000000000000000".HexToBytes();
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
await ntag.SetupBoltcard("lnurlw://blahblah.com", BoltcardKeys.Default, keys);
var uri = await ntag.TryReadNDefURI();
var piccData = issuerKey.TryDecrypt(uri);
Assert.NotNull(piccData);
// In real life, you would fetch the nonce from database
// var nonce = await FetchNonce(issuerKey.GetId(piccData.Uid));
Assert.True(issuerKey.CheckSunMac(uri, piccData, nonce));
await ntag.ResetCard(issuerKey, nonce);
// 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, nonce);`.
}
[Fact]
public async Task Reset()
{
var issuerKey = new AESKey("01000000000000000000000000000000".HexToBytes());
var batchKeys = new DeterministicBatchKeys(issuerKey);
var issuerKey = new IssuerKey("01000000000000000000000000000000".HexToBytes());
var nonce = "00010000000000000000000000000000".HexToBytes();
using var ctx = PCSCContext.Create();
var enc = batchKeys.DeriveEncryptionKey();
var ntag = ctx.CreateNTag424();
await ntag.AuthenticateEV2First(1, enc);
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var keys = batchKeys.DeriveBoltcardKeys(uid);
await ntag.ResetCard(keys);
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
await ntag.SetupBoltcard("lnurlw://test.com", BoltcardKeys.Default, keys);
await ntag.ResetCard(issuerKey, nonce);
}
[Fact]
@ -226,7 +260,7 @@ public class UnitTest1
await ntag.SetupBoltcard("http://test.com", BoltcardKeys.Default, keys);
var uri = await ntag.TryReadNDefURI();
Assert.StartsWith("lnurlw://test.com/?p=", uri?.AbsoluteUri);
var piccData = PICCData.TryBoltcardDecrypt(keys.EncryptionKey, keys.AuthenticationKey, uri);
var piccData = PICCData.TryBoltcardDecryptCheck(keys.EncryptionKey, keys.AuthenticationKey, uri);
Assert.NotNull(piccData);
await ntag.ResetCard(keys);
}
@ -236,18 +270,19 @@ public class UnitTest1
{
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
var batchKeys = new DeterministicBatchKeys(issuerKey);
//await ntag.ResetCard(batchKeys);
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
var nonce = "00010000000000000000000000000000".HexToBytes();
//await ntag.ResetCard(issuerKey, nonce);
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var keys = batchKeys.DeriveBoltcardKeys(uid);
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
await ntag.SetupBoltcard("http://test.com", BoltcardKeys.Default, keys);
var uri = await ntag.TryReadNDefURI();
var piccData = PICCData.TryDeterministicBoltcardDecrypt(batchKeys, uri);
issuerKey.TryDecrypt(uri);
var piccData = issuerKey.TryDecrypt(uri);
Assert.NotNull(piccData);
await ntag.ResetCard(batchKeys);
await ntag.ResetCard(issuerKey, nonce);
}
[Fact]