diff --git a/src/Hydrogen/Crypto/VRF/VRF.cs b/src/Hydrogen/Crypto/VRF/VRF.cs index 306f098d..c260c072 100644 --- a/src/Hydrogen/Crypto/VRF/VRF.cs +++ b/src/Hydrogen/Crypto/VRF/VRF.cs @@ -13,16 +13,16 @@ namespace Hydrogen; public class VRF { - public static IVRFAlgorithm CreateCryptographicVRF(DSS signatureScheme, CHF hasher) - => new CryptographicVRF(signatureScheme, hasher); + public static CryptographicVRF CreateCryptographicVRF(CHF chf, DSS dss) + => new CryptographicVRF(dss, chf); - public static byte[] Generate(DSS signatureScheme, CHF hasher, ReadOnlySpan seed, IPrivateKey privateKey, out byte[] proof, ulong nonce = 0UL) - => CreateCryptographicVRF(signatureScheme, hasher).Run(seed, privateKey, nonce, out proof); + public static byte[] Generate(CHF chf, DSS dss, ReadOnlySpan seed, IPrivateKey privateKey, out byte[] proof, ulong nonce = 0UL) + => CreateCryptographicVRF(chf, dss).Run(seed, privateKey, nonce, out proof); - public static bool TryVerify(DSS signatureScheme, CHF hasher, ReadOnlySpan seed, ReadOnlySpan output, ReadOnlySpan proof, IPublicKey publicKey) - => CreateCryptographicVRF(signatureScheme, hasher).TryVerify(seed, output, proof, publicKey); + public static bool TryVerify(CHF chf, DSS dss, ReadOnlySpan seed, ReadOnlySpan output, ReadOnlySpan proof, IPublicKey publicKey) + => CreateCryptographicVRF(chf, dss).TryVerify(seed, output, proof, publicKey); - public static void VerifyOrThrow(DSS signatureScheme, CHF hasher, ReadOnlySpan seed, ReadOnlySpan output, ReadOnlySpan proof, IPublicKey publicKey) - => CreateCryptographicVRF(signatureScheme, hasher).VerifyProofOrThrow(seed, output, proof, publicKey); + public static void VerifyOrThrow(CHF chf, DSS dss, ReadOnlySpan seed, ReadOnlySpan output, ReadOnlySpan proof, IPublicKey publicKey) + => CreateCryptographicVRF(chf, dss).VerifyProofOrThrow(seed, output, proof, publicKey); } diff --git a/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs b/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs index cd489166..9ef98639 100644 --- a/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs +++ b/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs @@ -26,6 +26,10 @@ public class VerfiableRandom : IRandomNumberGenerator { public byte[] VRFOutput { get; } + public VerfiableRandom(CHF chf, DSS dss, ReadOnlySpan seed, IPrivateKey privateKey, ulong nonce = 0L) + : this(Hydrogen.VRF.CreateCryptographicVRF(chf, dss), seed, privateKey, nonce) { + } + public VerfiableRandom(CryptographicVRF vrf, ReadOnlySpan seed, IPrivateKey privateKey, ulong nonce = 0L) : this(vrf.CHF, vrf, seed, privateKey, nonce) { } @@ -39,21 +43,23 @@ public VerfiableRandom(CHF chf, IVRFAlgorithm vrf, ReadOnlySpan seed, IPri _rng = new HashRandom(chf, VRFOutput); } - public VerfiableRandom(CryptographicVRF vrf, ReadOnlySpan seed, IPublicKey publicKey, byte[] unverifiedProof) + public VerfiableRandom(CHF chf, DSS dss, ReadOnlySpan seed, IPublicKey publicKey, ReadOnlySpan unverifiedProof) + : this(Hydrogen.VRF.CreateCryptographicVRF(chf, dss), seed, publicKey, unverifiedProof) { + } + + public VerfiableRandom(CryptographicVRF vrf, ReadOnlySpan seed, IPublicKey publicKey, ReadOnlySpan unverifiedProof) : this(vrf.CHF, vrf, seed, vrf.CalculateOutput(unverifiedProof), publicKey, unverifiedProof) { } - public VerfiableRandom(CHF chf, IVRFAlgorithm vrf, ReadOnlySpan seed, ReadOnlySpan output, IPublicKey publicKey, byte[] unverifiedProof) { + public VerfiableRandom(CHF chf, IVRFAlgorithm vrf, ReadOnlySpan seed, ReadOnlySpan output, IPublicKey publicKey, ReadOnlySpan unverifiedProof) { vrf.VerifyProofOrThrow(seed, output, unverifiedProof, publicKey); CHF = chf; VRF = vrf; VRFSeed = seed.ToArray(); - VRFProof = unverifiedProof; + VRFProof = unverifiedProof.ToArray(); VRFOutput = output.ToArray(); _rng = new HashRandom(chf, VRFOutput); } - public byte[] NextBytes(int count) { - return _rng.NextBytes(count); - } + public void NextBytes(Span result) => _rng.NextBytes(result); } diff --git a/src/Hydrogen/Maths/ErrorBandEqualityComparer.cs b/src/Hydrogen/Maths/ErrorBandEqualityComparer.cs new file mode 100644 index 00000000..78c8a999 --- /dev/null +++ b/src/Hydrogen/Maths/ErrorBandEqualityComparer.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Net.Http.Headers; + +namespace Hydrogen.Maths; + +public class ErrorBandEqualityComparer : IEqualityComparer { + private readonly decimal _tolerance; + + public ErrorBandEqualityComparer(decimal tolerance) { + if (tolerance < 0) { + throw new ArgumentOutOfRangeException(nameof(tolerance), "Tolerance must be non-negative."); + } + _tolerance = tolerance; + } + + public bool Equals(decimal x, decimal y) { + return Math.Abs(x - y) <= _tolerance; + } + + public int GetHashCode(decimal obj) => throw new NotSupportedException("Reason: loss of equivalence under transitivity due to the inexact nature of the comparison"); +} \ No newline at end of file diff --git a/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs b/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs index 0fa0fbc0..e1630962 100644 --- a/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs +++ b/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs @@ -15,7 +15,7 @@ namespace Hydrogen.CryptoEx.Tests; public class VRFTests { private IVRFAlgorithm BuildVRF(DSS dss, CHF chf) - => VRF.CreateCryptographicVRF(dss, chf); + => VRF.CreateCryptographicVRF(chf, dss); [Test] public void ValidProofAndOutputsPass( diff --git a/tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs b/tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs new file mode 100644 index 00000000..e9dff57d --- /dev/null +++ b/tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) Sphere 10 Software. All rights reserved. (https://sphere10.com) +// Author: Herman Schoenfeld +// +// Distributed under the MIT software license, see the accompanying file +// LICENSE or visit http://www.opensource.org/licenses/mit-license.php. +// +// This notice must not be removed when duplicating this file or its contents, in whole or in part. + +using System; +using Hydrogen.Maths; +using NUnit.Framework; + +namespace Hydrogen.CryptoEx.Tests; + +[TestFixture] +public class VerifiableRandomTests { + + [Test] + public void Integration( + [Values(DSS.ECDSA_SECP256k1, DSS.ECDSA_SECP384R1, DSS.ECDSA_SECP521R1, DSS.ECDSA_SECT283K1, DSS.PQC_WAMS, DSS.PQC_WAMSSharp)] + DSS dss, + + [Values(CHF.SHA2_256, CHF.SHA2_512, CHF.SHA3_256, CHF.Blake2b_256, CHF.Blake2b_128)] + CHF chf, + + [Values(0UL, 1UL, 111UL)] + ulong nonce, + + [Values(32, 256, 1024)] + int seedLen + ) { + var rng = new Random(31337); + var seed = rng.NextBytes(seedLen); + + var privateKey = Signers.GeneratePrivateKey(dss); + var publicKey = Signers.DerivePublicKey(dss, privateKey, nonce); + + + // Generate 1MB of random bytes using private key + var sourceGenerator = new VerfiableRandom(chf, dss, seed, privateKey, nonce); + var sourceBytes = sourceGenerator.NextBytes(1048576); + + // Re-generate 1mb of random bytes using public key + var verifierGenerator = new VerfiableRandom(chf, dss, seed, publicKey, sourceGenerator.VRFProof); + var destBytes = verifierGenerator.NextBytes(1048576); + + // Ensure bytes generated same + Assert.That(destBytes, Is.EqualTo(sourceBytes).Using(ByteArrayEqualityComparer.Instance)); + + // Ensure statistically random + var globalStats = new Statistics(); + var stats = new Statistics[256]; + for(var i = 0; i < 256; i++) { + globalStats.AddDatum(sourceBytes[i]); + stats[i] = new Statistics(); + for (var j = 0; j < 256; j++) { + stats[i].AddDatum(i == j ? 1 : 0); + } + } + + // NOTE: error margin of 20 is used here but this is due to loss of precision in global stats, there's lots of doubles being aggregated + // the below stats are more accurate + Assert.That((decimal)globalStats.Mean, Is.EqualTo(128M).Using(new ErrorBandEqualityComparer(20M)), $"Expected mean 128 (+/- 20) but was {globalStats.Mean}"); + + // Ensure every byte occured with equal probability + var byteStatComparer = new ErrorBandEqualityComparer(0.000001M); + for(var i = 0; i < 256; i++) + Assert.That((decimal)stats[i].Mean, Is.EqualTo(1/256M).Using(byteStatComparer), $"Byte {i} expected mean 1/256 (+/- 0.000001) but was {stats[i].Mean}"); + + } + + + [Test] + public void IntegrationFails( + [Values(DSS.ECDSA_SECP256k1, DSS.ECDSA_SECP384R1, DSS.ECDSA_SECP521R1, DSS.ECDSA_SECT283K1, DSS.PQC_WAMS, DSS.PQC_WAMSSharp)] + DSS dss, + + [Values(CHF.SHA2_256, CHF.SHA2_512, CHF.SHA3_256, CHF.Blake2b_256, CHF.Blake2b_128)] + CHF chf, + + [Values(0UL, 1UL, 111UL)] + ulong nonce, + + [Values(32, 256, 1024)] + int seedLen + ) { + var rng = new Random(31337); + var seed = rng.NextBytes(seedLen); + + var privateKey = Signers.GeneratePrivateKey(dss); + var publicKey = Signers.DerivePublicKey(dss, privateKey, nonce); + + + var sourceGenerator = new VerfiableRandom(chf, dss, seed, privateKey, nonce); + + // vary a seed byte randomly (try every index) + var badSeed = Tools.Array.Clone(sourceGenerator.VRFSeed); + for (var i = 0; i < seed.Length; i++) { + while ((badSeed[i] = rng.NextByte()) == sourceGenerator.VRFSeed[i]); + Assert.That(() => new VerfiableRandom(chf, dss, badSeed, publicKey, sourceGenerator.VRFProof), Throws.InvalidOperationException); + } + + // vary proof byte randomly (try every index) + var badProof = Tools.Array.Clone(sourceGenerator.VRFProof); + for (var i = 0; i < sourceGenerator.VRFProof.Length; i++) { + while ((badProof[i] = rng.NextByte()) == sourceGenerator.VRFProof[i]); + Assert.That(() => new VerfiableRandom(chf, dss, sourceGenerator.VRFSeed, publicKey, badProof), Throws.InvalidOperationException); + } + + } +}