Decouple HardwareWalletService into two classes: LedgerHardwareWalletService and HardwareWalletService

This commit is contained in:
nicolas.dorier
2019-05-10 10:48:30 +09:00
parent 01e5b319d1
commit 75f2749b19
3 changed files with 182 additions and 181 deletions

View File

@@ -557,22 +557,18 @@ namespace BTCPayServer.Controllers
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched"); throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation);
// Some deployment does not have the AccountKeyPath set, let's fix this... // Some deployment does not have the AccountKeyPath set, let's fix this...
if (derivationSettings.AccountKeyPath == null) if (derivationSettings.AccountKeyPath == null)
{ {
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
var foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token); var foundKeyPath = await hw.FindKeyPathFromPubkeys(network,
if (foundKeyPath == null) derivationSettings.AccountDerivation.GetExtPubKeys().Select(p => p.GetPublicKey()).ToArray(),
throw new HardwareWalletException($"This store is not configured to use this ledger"); normalOperationTimeout.Token);
derivationSettings.AccountKeyPath = foundKeyPath; derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
storeData.SetSupportedPaymentMethod(derivationSettings); storeData.SetSupportedPaymentMethod(derivationSettings);
await Repository.UpdateStore(storeData); await Repository.UpdateStore(storeData);
} }
// If it has the AccountKeyPath, let's check if we opened the right ledger // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
else else
{ {
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub, // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
@@ -587,8 +583,8 @@ namespace BTCPayServer.Controllers
// We have the root fingerprint, we can check the root from it // We have the root fingerprint, we can check the root from it
else else
{ {
var actualPubKey = await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token); var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
if (actualPubKey.GetPublicKey().GetHDFingerPrint() != derivationSettings.RootFingerprint.Value) if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
throw new HardwareWalletException($"This store is not configured to use this ledger"); throw new HardwareWalletException($"This store is not configured to use this ledger");
} }
} }
@@ -596,7 +592,7 @@ namespace BTCPayServer.Controllers
// Some deployment does not have the RootFingerprint set, let's fix this... // Some deployment does not have the RootFingerprint set, let's fix this...
if (derivationSettings.RootFingerprint == null) if (derivationSettings.RootFingerprint == null)
{ {
derivationSettings.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetPublicKey().GetHDFingerPrint(); derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
storeData.SetSupportedPaymentMethod(derivationSettings); storeData.SetSupportedPaymentMethod(derivationSettings);
await Repository.UpdateStore(storeData); await Repository.UpdateStore(storeData);
} }
@@ -648,16 +644,6 @@ namespace BTCPayServer.Controllers
} }
return new EmptyResult(); return new EmptyResult();
} }
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
{
if (strategy == null)
throw new Exception("The derivation scheme is not provided");
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
return directStrategy;
}
} }

View File

@@ -20,189 +20,46 @@ namespace BTCPayServer.Services
public HardwareWalletException(string message) : base(message) { } public HardwareWalletException(string message) : base(message) { }
public HardwareWalletException(string message, Exception inner) : base(message, inner) { } public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
} }
public class HardwareWalletService : IDisposable public abstract class HardwareWalletService : IDisposable
{ {
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable public abstract string Device { get; }
public abstract Task<LedgerTestResult> Test(CancellationToken cancellation);
public abstract Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation);
public virtual async Task<PubKey> GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
{ {
private readonly WebSocket webSocket; return (await GetExtPubKey(network, keyPath, cancellation)).GetPublicKey();
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
{
if (webSocket == null)
throw new ArgumentNullException(nameof(webSocket));
this.webSocket = webSocket;
}
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
{
await _Semaphore.WaitAsync();
List<byte[]> responses = new List<byte[]>();
try
{
foreach (var apdu in apdus)
{
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
}
finally
{
_Semaphore.Release();
}
return responses.ToArray();
}
public void Dispose()
{
_Semaphore.Dispose();
}
} }
private readonly LedgerClient _Ledger; public async Task<KeyPath> FindKeyPathFromPubkeys(BTCPayNetwork network, PubKey[] pubKeys, CancellationToken cancellation)
public LedgerClient Ledger
{
get
{
return _Ledger;
}
}
public string Device => "Ledger wallet";
WebSocketTransport _Transport = null;
public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
{
if (ledgerWallet == null)
throw new ArgumentNullException(nameof(ledgerWallet));
_Transport = new WebSocketTransport(ledgerWallet);
_Ledger = new LedgerClient(_Transport);
_Ledger.MaxAPDUSize = 90;
}
public async Task<LedgerTestResult> Test(CancellationToken cancellation)
{
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
return new LedgerTestResult() { Success = true };
}
public async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
{
try
{
var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
try
{
pubKey.GetAddress(network.NBitcoinNetwork);
}
catch
{
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
}
var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(),
pubKey.ChainCode,
(byte)account.Indexes.Length,
parentFP,
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
return extpubkey;
}
catch (FormatException)
{
throw new HardwareWalletException("Unsupported ledger app");
}
}
public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
{ {
List<KeyPath> derivations = new List<KeyPath>(); List<KeyPath> derivations = new List<KeyPath>();
if (network.NBitcoinNetwork.Consensus.SupportSegwit) if (network.NBitcoinNetwork.Consensus.SupportSegwit)
derivations.Add(new KeyPath("49'")); derivations.Add(new KeyPath("49'"));
derivations.Add(new KeyPath("44'")); derivations.Add(new KeyPath("44'"));
derivations.Add(new KeyPath("84'"));
KeyPath foundKeyPath = null; KeyPath foundKeyPath = null;
foreach (var account in foreach (var account in
derivations derivations
.Select(purpose => purpose.Derive(network.CoinType)) .Select(purpose => purpose.Derive(network.CoinType))
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true)))) .SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
{ {
try var pubkey = await GetPubKey(network, account, cancellation);
if (pubKeys.Contains(pubkey))
{ {
var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation); foundKeyPath = account;
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) break;
{
foundKeyPath = account;
break;
}
}
catch (FormatException)
{
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
} }
} }
return foundKeyPath; return foundKeyPath;
} }
public async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint, public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
CancellationToken cancellationToken) CancellationToken cancellationToken);
{
try
{
var unsigned = psbt.GetGlobalTransaction();
var changeKeyPath = psbt.Outputs
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
.Where(o => o.HDKeyPaths.Any())
.Select(o => o.HDKeyPaths.First().Value.Item2)
.FirstOrDefault();
var signatureRequests = psbt
.Inputs
.Where(o => o.HDKeyPaths.Any())
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
.Select(i => new SignatureRequest()
{
InputCoin = i.GetSignableCoin(),
InputTransaction = i.NonWitnessUtxo,
KeyPath = i.HDKeyPaths.First().Value.Item2,
PubKey = i.HDKeyPaths.First().Key
}).ToArray();
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
if (signedTransaction == null)
throw new Exception("The ledger failed to sign the transaction");
psbt = psbt.Clone(); public virtual void Dispose()
foreach (var signature in signatureRequests)
{
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
if (input == null)
continue;
input.PartialSigs.Add(signature.PubKey, signature.Signature);
}
return psbt;
}
catch (Exception ex)
{
throw new Exception("The ledger failed to sign the transaction", ex);
}
}
public void Dispose()
{ {
if (_Transport != null)
_Transport.Dispose();
} }
} }

View File

@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using LedgerWallet;
using NBitcoin;
namespace BTCPayServer.Services
{
public class LedgerHardwareWalletService : HardwareWalletService
{
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
{
private readonly WebSocket webSocket;
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
{
if (webSocket == null)
throw new ArgumentNullException(nameof(webSocket));
this.webSocket = webSocket;
}
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
{
await _Semaphore.WaitAsync();
List<byte[]> responses = new List<byte[]>();
try
{
foreach (var apdu in apdus)
{
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
}
finally
{
_Semaphore.Release();
}
return responses.ToArray();
}
public void Dispose()
{
_Semaphore.Dispose();
}
}
private readonly LedgerClient _Ledger;
public LedgerClient Ledger
{
get
{
return _Ledger;
}
}
public override string Device => "Ledger wallet";
WebSocketTransport _Transport = null;
public LedgerHardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
{
if (ledgerWallet == null)
throw new ArgumentNullException(nameof(ledgerWallet));
_Transport = new WebSocketTransport(ledgerWallet);
_Ledger = new LedgerClient(_Transport);
_Ledger.MaxAPDUSize = 90;
}
public override async Task<LedgerTestResult> Test(CancellationToken cancellation)
{
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
return new LedgerTestResult() { Success = true };
}
public override async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return await GetExtPubKey(network, keyPath, false, cancellation);
}
public override async Task<PubKey> GetPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return (await GetExtPubKey(network, keyPath, false, cancellation)).GetPublicKey();
}
private async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
{
var pubKey = await Ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
try
{
pubKey.GetAddress(network.NBitcoinNetwork);
}
catch
{
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new HardwareWalletException($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
}
var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await Ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(),
pubKey.ChainCode,
(byte)account.Indexes.Length,
parentFP,
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
return extpubkey;
}
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken)
{
var unsigned = psbt.GetGlobalTransaction();
var changeKeyPath = psbt.Outputs
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
.Where(o => o.HDKeyPaths.Any())
.Select(o => o.HDKeyPaths.First().Value.Item2)
.FirstOrDefault();
var signatureRequests = psbt
.Inputs
.Where(o => o.HDKeyPaths.Any())
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
.Select(i => new SignatureRequest()
{
InputCoin = i.GetSignableCoin(),
InputTransaction = i.NonWitnessUtxo,
KeyPath = i.HDKeyPaths.First().Value.Item2,
PubKey = i.HDKeyPaths.First().Key
}).ToArray();
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
if (signedTransaction == null)
throw new HardwareWalletException("The ledger failed to sign the transaction");
psbt = psbt.Clone();
foreach (var signature in signatureRequests)
{
var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint);
if (input == null)
continue;
input.PartialSigs.Add(signature.PubKey, signature.Signature);
}
return psbt;
}
public override void Dispose()
{
if (_Transport != null)
_Transport.Dispose();
}
}
}