Implement proof of ownership
ADD
WalletWasabi.Tests/UnitTests/Crypto/OwnershipProofTest.cs
+using NBitcoin;
+using NBitcoin.DataEncoders;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using WalletWasabi.Crypto;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Crypto
+{
+ public class OwnershipProofTest
+ {
+ [Fact]
+ public void OwnershipProofEncodingDecoding()
+ {
+ // Trezor test vector
+ // [all all all all all all all all all all all all]/84'/0'/0'/1/0
+ using var key = new Key(Encoders.Hex.DecodeData("3460814214450E864EC722FF1F84F96C41746CD6BBE2F1C09B33972761032E9F"));
+ var ownershipIdentifier = new OwnershipIdentifier(Encoders.Hex.DecodeData("A122407EFC198211C81AF4450F40B235D54775EFD934D16B9E31C6CE9BAD5707"));
+ var commitmentData = new byte[0];
+ var ownershipProof = OwnershipProof.Generate(key, ownershipIdentifier, commitmentData, false, ScriptPubKeyType.Segwit);
+ var scriptPubKey = PayToWitPubKeyHashTemplate.Instance.GenerateScriptPubKey(key.PubKey);
+
+ var ownershipProofBytes = Encoders.Hex.DecodeData("534c00190001a122407efc198211c81af4450f40b235d54775efd934d16b9e31c6ce9bad57070002483045022100e5eaf2cb0a473b4545115c7b85323809e75cb106175ace38129fd62323d73df30220363dbc7acb7afcda022b1f8d97acb8f47c42043cfe0595583aa26e30bc8b3bb50121032ef68318c8f6aaa0adec0199c69901f0db7d3485eb38d9ad235221dc3d61154b");
+
+ Assert.True(ownershipProofBytes.SequenceEqual(ownershipProof.ToBytes()));
+ var deserializedOwnershipProof = OwnershipProof.FromBytes(ownershipProofBytes);;
+ Assert.True(ownershipProofBytes.SequenceEqual(deserializedOwnershipProof.ToBytes()));
+ }
+
+ [Fact]
+ public void OwnershipProofVerification()
+ {
+ // [all all all all all all all all all all all all]/84'/0'/0'/1/0
+ using var key = new Key(Encoders.Hex.DecodeData("3460814214450E864EC722FF1F84F96C41746CD6BBE2F1C09B33972761032E9F"));
+ var ownershipIdentifier = new OwnershipIdentifier(Encoders.Hex.DecodeData("A122407EFC198211C81AF4450F40B235D54775EFD934D16B9E31C6CE9BAD5707"));
+ var commitmentData = Encoders.Hex.DecodeData("A42E38EF564D4B05B65575D22553BB1F264332D77F8A61159ABF3E6179B0317C");
+ var scriptPubKey = PayToWitPubKeyHashTemplate.Instance.GenerateScriptPubKey(key.PubKey);
+
+ OwnershipProof ownershipProof, invalidOwnershipProof;
+
+ // Valid proofs
+ ownershipProof = OwnershipProof.Generate(key, ownershipIdentifier, commitmentData, true, ScriptPubKeyType.Segwit);
+ Assert.True(ownershipProof.VerifyOwnership(scriptPubKey, commitmentData, false));
+ Assert.True(ownershipProof.VerifyOwnership(scriptPubKey, commitmentData, true));
+
+ ownershipProof = OwnershipProof.Generate(key, ownershipIdentifier, commitmentData, false, ScriptPubKeyType.Segwit);
+ Assert.True(ownershipProof.VerifyOwnership(scriptPubKey, commitmentData, false));
+
+ // Valid proof, modified user confirmation flag
+ Assert.False(ownershipProof.VerifyOwnership(scriptPubKey, commitmentData, true));
+
+ // Valid proof, modified commitment data
+ var invalidCommitmentData = (byte[]) commitmentData.Clone();
+ invalidCommitmentData[0] ^= (byte) 1;
+ Assert.False(ownershipProof.VerifyOwnership(scriptPubKey, invalidCommitmentData, false));
+
+ // Valid proof, invalid scriptPubKey
+ // [all all all all all all all all all all all all]/84'/0'/0'/1/1
+ var invalidKey = new Key(Encoders.Hex.DecodeData("7b041dd735e7202d3c1b9592147894ed24da6355f0cd66573c273c0df1afa78a"));
+ var invalidScriptPubKey = PayToWitPubKeyHashTemplate.Instance.GenerateScriptPubKey(invalidKey.PubKey);
+ Assert.False(ownershipProof.VerifyOwnership(invalidScriptPubKey, commitmentData, false));
+
+ // Invalid proof, modified ownership identifier
+ var invalidOwnershipIdentifierBytes = ownershipIdentifier.ToBytes();
+ invalidOwnershipIdentifierBytes[0] ^= (byte) 1;
+
+ invalidOwnershipProof = new OwnershipProof(
+ new ProofBody(ownershipProof.ProofBody.Flags, new OwnershipIdentifier(invalidOwnershipIdentifierBytes)),
+ ownershipProof.ProofSignature);
+
+ Assert.False(invalidOwnershipProof.VerifyOwnership(scriptPubKey, commitmentData, false));
+
+ // Invalid proof, modified r part of signature in witness
+ var invalidSignature = ownershipProof.ProofSignature.Witness[0].ToArray();
+ invalidSignature[4] ^= (byte) 1;
+
+ invalidOwnershipProof = new OwnershipProof(
+ ownershipProof.ProofBody,
+ new Bip322Signature(ownershipProof.ProofSignature.ScriptSig, new WitScript(new byte[][] { invalidSignature, ownershipProof.ProofSignature.Witness[1] } )));
+
+ Assert.False(invalidOwnershipProof.VerifyOwnership(scriptPubKey, commitmentData, false));
+ }
+ }
+}

ADD
WalletWasabi/Crypto/OwnershipProof.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NBitcoin;
+using NBitcoin.Crypto;
+
+namespace WalletWasabi.Crypto
+{
+ public class OwnershipIdentifier : IBitcoinSerializable
+ {
+ public const int OwnershipIdLength = 32;
+ private byte[] bytes;
+
+ public OwnershipIdentifier(byte[] bytes)
+ {
+ if (bytes.Length != OwnershipIdLength)
+ throw new ArgumentException($"Ownership identifier must be {OwnershipIdLength} bytes long.");
+
+ this.bytes = bytes.ToArray();
+ }
+
+ public OwnershipIdentifier()
+ : this(new byte[OwnershipIdLength])
+ {
+ }
+
+ public void ReadWrite(BitcoinStream bitcoinStream)
+ {
+ bitcoinStream.ReadWrite(ref bytes);
+ }
+ }
+
+ public class CoinJoinInputCommitmentData
+ {
+ private byte[] coordinatorIdentifier;
+ private long roundIdentifier;
+
+ public CoinJoinInputCommitmentData(string coordinatorIdentifier, long roundIdentifier)
+ : this (Encoding.ASCII.GetBytes(coordinatorIdentifier), roundIdentifier)
+ {
+ }
+
+ public CoinJoinInputCommitmentData(byte[] coordinatorIdentifier, long roundIdentifier)
+ {
+ this.coordinatorIdentifier = coordinatorIdentifier;
+ this.roundIdentifier = roundIdentifier;
+ }
+
+ public byte[] ToBytes() =>
+ BitConverter.GetBytes(coordinatorIdentifier.Length)
+ .Concat(coordinatorIdentifier)
+ .Concat(BitConverter.GetBytes(roundIdentifier))
+ .ToArray();
+ }
+
+ [Flags]
+ public enum ProofBodyFlags : byte
+ {
+ UserConfirmation = 1
+ }
+
+ public class ProofBody : IBitcoinSerializable
+ {
+ private static readonly byte[] VersionMagic = { 0x53, 0x4c, 0x00, 0x19 };
+ private List<OwnershipIdentifier> ownershipIdentifiers = new();
+ private byte flags;
+
+ public ProofBody()
+ {
+ }
+
+ public ProofBody(ProofBodyFlags flags, params OwnershipIdentifier[] ownershipIdentifiers)
+ {
+ Flags = flags;
+ this.ownershipIdentifiers = ownershipIdentifiers.ToList();
+ }
+
+ public IEnumerable<OwnershipIdentifier> OwnershipIdentifiers => ownershipIdentifiers;
+
+ public ProofBodyFlags Flags
+ {
+ get => (ProofBodyFlags)flags;
+ set => flags = (byte)value;
+ }
+
+ public void ReadWrite(BitcoinStream bitcoinStream)
+ {
+ var versionMagic = VersionMagic.ToArray();
+ bitcoinStream.ReadWrite(ref versionMagic);
+
+ if (!bitcoinStream.Serializing)
+ {
+ if (!VersionMagic.SequenceEqual(versionMagic))
+ {
+ throw new FormatException("Invalid version magic.");
+ }
+ }
+
+ bitcoinStream.ReadWrite(ref flags);
+ bitcoinStream.ReadWrite(ref ownershipIdentifiers);
+ }
+
+ public uint256 SignatureHash(Script scriptPubKey, byte[] commitmentData) =>
+ new uint256(Hashes.SHA256(this.ToBytes().Concat(ProofFooter(scriptPubKey, commitmentData)).ToArray()));
+
+ private static IEnumerable<byte> ProofFooter(Script scriptPubKey, byte[] commitmentData) =>
+ scriptPubKey.ToBytes().Concat(commitmentData);
+ }
+
+ public class OwnershipProof : IBitcoinSerializable
+ {
+ private ProofBody proofBody;
+ private Bip322Signature proofSignature;
+
+ public OwnershipProof()
+ : this(new ProofBody(), new Bip322Signature())
+ {
+ }
+
+ public OwnershipProof(ProofBody proofBody, Bip322Signature proofSignature)
+ {
+ this.proofBody = proofBody;
+ this.proofSignature = proofSignature;
+ }
+
+ public ProofBody ProofBody => proofBody;
+
+ public Bip322Signature ProofSignature => proofSignature;
+
+ public void ReadWrite(BitcoinStream bitcoinStream)
+ {
+ bitcoinStream.ReadWrite(ref proofBody);
+ bitcoinStream.ReadWrite(ref proofSignature);
+ }
+
+ public static OwnershipProof Generate(Key key, OwnershipIdentifier ownershipIdentifier, byte[] commitmentData, bool userConfirmation, ScriptPubKeyType scriptPubKeyType) =>
+ scriptPubKeyType switch
+ {
+ ScriptPubKeyType.Segwit => GenerateOwnershipProofSegwit(key, commitmentData, new ProofBody(userConfirmation ? ProofBodyFlags.UserConfirmation : 0, ownershipIdentifier)),
+ _ => throw new NotImplementedException()
+ };
+
+ private static OwnershipProof GenerateOwnershipProofSegwit(Key key, byte[] commitmentData, ProofBody proofBody) =>
+ new OwnershipProof(
+ proofBody,
+ Bip322Signature.Generate(key, proofBody.SignatureHash(key.PubKey.WitHash.ScriptPubKey, commitmentData), ScriptPubKeyType.Segwit));
+
+ public bool VerifyOwnership(Script scriptPubKey, byte[] commitmentData, bool requireUserConfirmation) =>
+ scriptPubKey.IsScriptType(ScriptType.P2WPKH) switch
+ {
+ true => VerifyOwnershipProofSegwit(scriptPubKey, commitmentData, requireUserConfirmation),
+ false => throw new NotImplementedException()
+ };
+
+ private bool VerifyOwnershipProofSegwit(Script scriptPubKey, byte[] commitmentData, bool requireUserConfirmation)
+ {
+ if (requireUserConfirmation && !proofBody.Flags.HasFlag(ProofBodyFlags.UserConfirmation))
+ return false;
+
+ var hash = proofBody.SignatureHash(scriptPubKey, commitmentData);
+
+ return proofSignature.Verify(hash, scriptPubKey);
+ }
+
+ public static OwnershipProof GenerateCoinJoinInputProof(Key key, CoinJoinInputCommitmentData coinJoinInputsCommitmentData) =>
+ Generate(key, new OwnershipIdentifier(), coinJoinInputsCommitmentData.ToBytes(), true, ScriptPubKeyType.Segwit);
+
+ public static bool VerifyCoinJoinInputProof(byte[] ownershipProofBytes, Script scriptPubKey, CoinJoinInputCommitmentData coinJoinInputsCommitmentData) =>
+ FromBytes(ownershipProofBytes).VerifyOwnership(scriptPubKey, coinJoinInputsCommitmentData.ToBytes(), true);
+
+ public static OwnershipProof FromBytes(byte[] ownershipProofBytes) =>
+ NBitcoinExtensions.FromBytes<OwnershipProof>(ownershipProofBytes);
+ }
+}

Implement SLIP-19 proof of ownership