From dbe2debb6736665919f2e224b964bff3abfe5b8d Mon Sep 17 00:00:00 2001 From: Max Raab Date: Sun, 16 Nov 2025 12:22:39 +0100 Subject: [PATCH 1/2] Add hashed signing methods Introduce new methods for creating, loading and validating hashed signatures. --- Minisign.Net.Tests/BaseTests.cs | 65 +++++- Minisign.Net.Tests/Data/test.jpg.minisig | 6 +- .../Data/test.jpg.minisig-hashed | 4 - .../Data/test.jpg.minisig-legacy | 4 + Minisign.Net.Tests/Data/test2.key | 2 - Minisign.Net.Tests/Data/test2.pub | 2 - Minisign.Net.Tests/Minisign.Net.Tests.csproj | 9 +- Minisign.Net/Core.cs | 220 +++++++++++++++--- Minisign.Net/Models/MinisignSignature.cs | 1 - README.md | 12 +- 10 files changed, 257 insertions(+), 68 deletions(-) delete mode 100644 Minisign.Net.Tests/Data/test.jpg.minisig-hashed create mode 100644 Minisign.Net.Tests/Data/test.jpg.minisig-legacy delete mode 100644 Minisign.Net.Tests/Data/test2.key delete mode 100644 Minisign.Net.Tests/Data/test2.pub diff --git a/Minisign.Net.Tests/BaseTests.cs b/Minisign.Net.Tests/BaseTests.cs index 9bbba84..69b63a4 100644 --- a/Minisign.Net.Tests/BaseTests.cs +++ b/Minisign.Net.Tests/BaseTests.cs @@ -27,7 +27,7 @@ public void GenerateKeyTest() } [Fact] - public void SignTest() + public void SignAndValidataLegacyTest() { const string expected = "9d6f33b5e347042e"; const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; @@ -35,19 +35,19 @@ public void SignTest() var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); var file = Path.Combine("Data", "testfile.jpg"); - var signedFile = Core.Sign(file, minisignPrivateKey); + var signedFile = Core.SignLegacy(file, minisignPrivateKey); var minisignSignature = Core.LoadSignatureFromFile(signedFile); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); - Assert.True(Core.ValidateSignature(file, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateLegacySignature(file, minisignSignature, minisignPublicKey)); File.Delete(signedFile); } [Fact] - public void Sign2Test() + public void SignAndValidateLegacy2Test() { const string expected = "9d6f33b5e347042e"; const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; @@ -56,14 +56,55 @@ public void Sign2Test() var file = Path.Combine("Data", "testfile.jpg"); var fileBinary = File.ReadAllBytes(file); - var signedFile = Core.Sign(file, minisignPrivateKey); + var signedFile = Core.SignLegacy(file, minisignPrivateKey); var minisignSignature = Core.LoadSignatureFromFile(signedFile); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); - Assert.True(Core.ValidateSignature(fileBinary, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateLegacySignature(fileBinary, minisignSignature, minisignPublicKey)); + File.Delete(signedFile); + } + + [Fact] + public void SignAndValidataHashedTest() + { + const string expected = "9d6f33b5e347042e"; + const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; + const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; + var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); + + var file = Path.Combine("Data", "testfile.jpg"); + var signedFile = Core.SignHashed(file, minisignPrivateKey); + + var minisignSignature = Core.LoadSignatureFromFile(signedFile); + var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); + Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); + Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); + + Assert.True(Core.ValidateHashedSignature(file, minisignSignature, minisignPublicKey)); + File.Delete(signedFile); + } + + [Fact] + public void SignAndValidateHashed2Test() + { + const string expected = "9d6f33b5e347042e"; + const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; + const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; + var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); + + var file = Path.Combine("Data", "testfile.jpg"); + var fileBinary = File.ReadAllBytes(file); + var signedFile = Core.SignHashed(file, minisignPrivateKey); + + var minisignSignature = Core.LoadSignatureFromFile(signedFile); + var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); + Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); + Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); + + Assert.True(Core.ValidateHashedSignature(fileBinary, minisignSignature, minisignPublicKey)); File.Delete(signedFile); } @@ -73,10 +114,10 @@ public void ValidateLegacySignature() var file = Path.Combine("Data", "testfile.jpg"); var fileBinary = File.ReadAllBytes(file); - var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig")); + var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig-legacy")); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); - Assert.True(Core.ValidateSignature(file, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateLegacySignature(file, minisignSignature, minisignPublicKey)); } [Fact] @@ -85,10 +126,10 @@ public void ValidateHashedSignature() var file = Path.Combine("Data", "testfile.jpg"); var fileBinary = File.ReadAllBytes(file); - var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig-hashed")); - var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test2.pub")); + var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig")); + var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); - Assert.True(Core.ValidateSignature(file, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateHashedSignature(file, minisignSignature, minisignPublicKey)); } [Fact] @@ -96,7 +137,7 @@ public void LoadSignatureFromStringTest() { const string expected = "9d6f33b5e347042e"; const string signatureString = "RWSdbzO140cELi+edKSQMZw/yrCDB3aetMNoPYsESNapZuUfHeE8JunmfFNykkZbXWRMy+0Y8aaONyhdGSZtbEXlw32RpDtMmgw="; - const string trustedComment = "trusted comment: timestamp: 1439294334 file: testfile.jpg"; + const string trustedComment = "trusted comment: timestamp: 1439294334 file: testfile.jpg"; const string globalSignature = "sXw0VdGKvIgZibPYp9bR5jz01dRkBbWzEBFLpY/+u7MGwk4HJT/Kj8aB1iXW3w6n9/gSv33cd2sk7uDVFclIAA=="; var minisignSignature = Core.LoadSignatureFromString(signatureString, trustedComment, globalSignature); Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); diff --git a/Minisign.Net.Tests/Data/test.jpg.minisig b/Minisign.Net.Tests/Data/test.jpg.minisig index 6bd60ef..11d0299 100644 --- a/Minisign.Net.Tests/Data/test.jpg.minisig +++ b/Minisign.Net.Tests/Data/test.jpg.minisig @@ -1,4 +1,4 @@ untrusted comment: signature from minisign secret key -RWSdbzO140cELi+edKSQMZw/yrCDB3aetMNoPYsESNapZuUfHeE8JunmfFNykkZbXWRMy+0Y8aaONyhdGSZtbEXlw32RpDtMmgw= -trusted comment: timestamp: 1439294334 file: testfile.jpg -sXw0VdGKvIgZibPYp9bR5jz01dRkBbWzEBFLpY/+u7MGwk4HJT/Kj8aB1iXW3w6n9/gSv33cd2sk7uDVFclIAA== +RUSdbzO140cELjvXFidzF+eQAAZBjdqVGnUCcfR0q/uK4Cq2ApHdQuX4W8n0VA/ep4+vRnwWD9wuH3rlSqvmHjqq9saALhfHTAs= +trusted comment: timestamp:1763291678 file:testfile.jpg hashed +EmgYQcdc0B/35ZL202oDGlTmczYv40NlWOatNxjCu4FboErErG1L8Fh6OMsxjw5NPb5+LYf9eH3Nng4Lg8+ECw== diff --git a/Minisign.Net.Tests/Data/test.jpg.minisig-hashed b/Minisign.Net.Tests/Data/test.jpg.minisig-hashed deleted file mode 100644 index 0cc2c25..0000000 --- a/Minisign.Net.Tests/Data/test.jpg.minisig-hashed +++ /dev/null @@ -1,4 +0,0 @@ -untrusted comment: signature from minisign secret key -RURkEyeJ3BlJXNvdwel3JozOQ7kDi59j2qqyCo0lqBLL54h8ThExW6T07L2rpO+L4DRTOiiDU+MRd26EZMqcFBUoslWhgvEUpgQ= -trusted comment: timestamp:1763131221 file:testfile.jpg hashed -0SOzbBbowMWp1KAfhLw7P8TtrdTUAflySfHvGTlcoMUedxRW2J3FJ7fF6TSAU55yxsKNjHDUKQocr9YK2IPjAw== diff --git a/Minisign.Net.Tests/Data/test.jpg.minisig-legacy b/Minisign.Net.Tests/Data/test.jpg.minisig-legacy new file mode 100644 index 0000000..7c7be18 --- /dev/null +++ b/Minisign.Net.Tests/Data/test.jpg.minisig-legacy @@ -0,0 +1,4 @@ +untrusted comment: signature from minisign secret key +RWSdbzO140cELve9DYyCHUaAHZ9x9L4TD5g4sze4h/myqaMr/jHyXO/SkUCXa17kzVk3hT7Pvntf5Yf2RXGY+YwaxZygyF+9nQ0= +trusted comment: timestamp:1763291664 file:testfile.jpg +eYe0V7nYy5w/zq/tFqOuJ4LhVH/Zy/38d2LphbWZCRQHfi0fMDSpEzg67sHZD9MMQX51TEs2SsDE2M7Z084CCA== diff --git a/Minisign.Net.Tests/Data/test2.key b/Minisign.Net.Tests/Data/test2.key deleted file mode 100644 index b1e1121..0000000 --- a/Minisign.Net.Tests/Data/test2.key +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign encrypted secret key -RWRTY0IyU6NgdxALuM7TmqLur9Vyow0JO/nq3K4EEgfMzAjsIxIAAAACAAAAAAAAAEAAAAAABSc/wG1tSf1x+XCTAwEImd1ajn3WFnsXo7vaQcV3XWVedffbKLRi5zGI0SdDcICtFgg2FS/gaVi6MAul0MhpD9JzEvJXblEKNQ1xJThMxoIl9loWfIOFFVTG41s5C3WRU4X0xA3y0vQ= diff --git a/Minisign.Net.Tests/Data/test2.pub b/Minisign.Net.Tests/Data/test2.pub deleted file mode 100644 index 6962f9b..0000000 --- a/Minisign.Net.Tests/Data/test2.pub +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign public key 5C4919DC89271364 -RWRkEyeJ3BlJXHYzPYxOBLrCP2/b9hBN7tExRifl7KMX9EQCKuPYt9zR diff --git a/Minisign.Net.Tests/Minisign.Net.Tests.csproj b/Minisign.Net.Tests/Minisign.Net.Tests.csproj index 7a46ad1..87e42bc 100644 --- a/Minisign.Net.Tests/Minisign.Net.Tests.csproj +++ b/Minisign.Net.Tests/Minisign.Net.Tests.csproj @@ -10,6 +10,7 @@ + @@ -19,7 +20,7 @@ PreserveNewest - + PreserveNewest @@ -28,12 +29,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - PreserveNewest diff --git a/Minisign.Net/Core.cs b/Minisign.Net/Core.cs index 3dc0081..43e5abf 100644 --- a/Minisign.Net/Core.cs +++ b/Minisign.Net/Core.cs @@ -20,7 +20,8 @@ public static class Core private const int TrustedCommentMaxBytes = 8192; private const int KeyNumBytes = 8; private const int KeySaltBytes = 32; - private const string Sigalg = "Ed"; + private const string SigalgLegacy = "Ed"; + private const string SigalgHashed = "ED"; private const string Kdfalg = "Sc"; private const string Chkalg = "B2"; private const string DefaultComment = "signature from minisign secret key"; @@ -53,7 +54,7 @@ public static class Core /// /// /// - public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", + public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") { if (fileToSign != null && !File.Exists(fileToSign)) @@ -73,7 +74,7 @@ public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateK { var timestamp = GetTimestamp(); var filename = Path.GetFileName(fileToSign); - trustedComment = "timestamp: " + timestamp + " file: " + filename; + trustedComment = "timestamp: " + timestamp + " file: " + filename; } if ((CommentPrefix + untrustedComment).Length > CommentMaxBytes) @@ -105,7 +106,110 @@ public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateK var minisignSignature = new MinisignSignature { KeyId = minisignPrivateKey.KeyId, - SignatureAlgorithm = Encoding.UTF8.GetBytes(Sigalg) + SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgLegacy) + }; + var signature = PublicKeyAuth.SignDetached(file, minisignPrivateKey.SecretKey); + minisignSignature.Signature = signature; + + var binarySignature = ArrayHelpers.ConcatArrays( + minisignSignature.SignatureAlgorithm, + minisignSignature.KeyId, + minisignSignature.Signature + ); + + // sign the signature and the trusted comment with a global signature + var globalSignature = + PublicKeyAuth.SignDetached( + ArrayHelpers.ConcatArrays(minisignSignature.Signature, Encoding.UTF8.GetBytes(trustedComment)), + minisignPrivateKey.SecretKey); + + // prepare the file lines + var signatureFileContent = new[] + { + CommentPrefix + untrustedComment, + Convert.ToBase64String(binarySignature), + TrustedCommentPrefix + trustedComment, + Convert.ToBase64String(globalSignature) + }; + + var outputFile = fileToSign + SigSuffix; + File.WriteAllLines(outputFile, signatureFileContent); + return outputFile; + } + + /// + /// Sign a file with a MinisignPrivateKey. + /// + /// The full path to the file. + /// A valid MinisignPrivateKey to sign. + /// An optional untrusted comment. + /// An optional trusted comment. + /// The folder to write the signature (optional). + /// The full path to the signed file. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", + string trustedComment = "", string outputFolder = "") + { + if (fileToSign != null && !File.Exists(fileToSign)) + { + throw new FileNotFoundException("could not find fileToSign"); + } + + if (minisignPrivateKey == null) + throw new ArgumentException("missing minisignPrivateKey input", nameof(minisignPrivateKey)); + + if (string.IsNullOrEmpty(untrustedComment)) + { + untrustedComment = DefaultComment; + } + + if (string.IsNullOrEmpty(trustedComment)) + { + var timestamp = GetTimestamp(); + var filename = Path.GetFileName(fileToSign); + trustedComment = "timestamp: " + timestamp + " file: " + filename + " hashed"; + } + + if ((CommentPrefix + untrustedComment).Length > CommentMaxBytes) + { + throw new ArgumentOutOfRangeException(nameof(untrustedComment), "untrustedComment too long"); + } + + if ((TrustedCommentPrefix + trustedComment).Length > TrustedCommentMaxBytes) + { + throw new ArgumentOutOfRangeException(nameof(trustedComment), "trustedComment too long"); + } + + if (string.IsNullOrEmpty(outputFolder)) + { + outputFolder = Path.GetDirectoryName(fileToSign); + } + + //validate the outputFolder + if (string.IsNullOrEmpty(outputFolder) || !Directory.Exists(outputFolder)) + { + throw new DirectoryNotFoundException("outputFolder must exist"); + } + + if (outputFolder.IndexOfAny(Path.GetInvalidPathChars()) > -1) + throw new ArgumentException("The given path to the output folder contains invalid characters!"); + + var file = ComputeBlake2bFileHash(fileToSign); + + var minisignSignature = new MinisignSignature + { + KeyId = minisignPrivateKey.KeyId, + SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgHashed) }; var signature = PublicKeyAuth.SignDetached(file, minisignPrivateKey.SecretKey); minisignSignature.Signature = signature; @@ -188,7 +292,7 @@ public static MinisignKeyPair GenerateKeyPair(string password, bool writeOutputF minisignPrivateKey.PublicKey = keyPair.PublicKey; minisignPrivateKey.KdfSalt = kdfSalt; - minisignPrivateKey.SignatureAlgorithm = Encoding.UTF8.GetBytes(Sigalg); + minisignPrivateKey.SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgLegacy); minisignPrivateKey.ChecksumAlgorithm = Encoding.UTF8.GetBytes(Chkalg); minisignPrivateKey.KdfAlgorithm = Encoding.UTF8.GetBytes(Kdfalg); minisignPrivateKey.KdfMemLimit = 1073741824; //currently unused @@ -214,7 +318,7 @@ public static MinisignKeyPair GenerateKeyPair(string password, bool writeOutputF { KeyId = keyId, PublicKey = keyPair.PublicKey, - SignatureAlgorithm = Encoding.UTF8.GetBytes(Sigalg) + SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgLegacy) }; keyPair.Dispose(); if (writeOutputFiles) @@ -275,7 +379,7 @@ public static MinisignKeyPair GenerateKeyPair(string password, bool writeOutputF /// /// /// - public static bool ValidateSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) + public static bool ValidateLegacySignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) { if (filePath != null && !File.Exists(filePath)) throw new FileNotFoundException("could not find filePath"); @@ -289,19 +393,10 @@ public static bool ValidateSignature(string filePath, MinisignSignature signatur if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; - if (!signature.IsHashed) - { - var file = LoadMessageFile(filePath); + var file = LoadMessageFile(filePath); - // Legacy: Ed25519(message) - if (!PublicKeyAuth.VerifyDetached(signature.Signature, file, publicKey.PublicKey)) return false; - } - else - { - // Hashed: Ed25519(Blake2b-512(message)) - var blake = ComputeBlake2bFileHash(filePath); - if (!PublicKeyAuth.VerifyDetached(signature.Signature, blake, publicKey.PublicKey)) return false; - } + // Legacy: Ed25519(message) + if (!PublicKeyAuth.VerifyDetached(signature.Signature, file, publicKey.PublicKey)) return false; // Global signature is the same for both formats return PublicKeyAuth.VerifyDetached( @@ -310,6 +405,41 @@ public static bool ValidateSignature(string filePath, MinisignSignature signatur publicKey.PublicKey); } + /// + /// Validate a file with a MinisignSignature and a MinisignPublicKey object. + /// + /// The full path to the file. + /// A valid MinisignSignature object. + /// A valid MinisignPublicKey object. + /// true if valid; otherwise, false. + /// + /// + /// + /// + /// + public static bool ValidateHashedSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) + { + if (filePath != null && !File.Exists(filePath)) + throw new FileNotFoundException("could not find filePath"); + + if (signature == null) + throw new ArgumentException("missing signature input", nameof(signature)); + + if (publicKey == null) + throw new ArgumentException("missing publicKey input", nameof(publicKey)); + + if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; + + // Hashed: Ed25519(Blake2b-512(message)) + var blake = ComputeBlake2bFileHash(filePath); + if (!PublicKeyAuth.VerifyDetached(signature.Signature, blake, publicKey.PublicKey)) return false; + + // Global signature is the same for both formats + return PublicKeyAuth.VerifyDetached( + signature.GlobalSignature, + ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), + publicKey.PublicKey); + } /// /// Validate a file with a MinisignSignature and a MinisignPublicKey object. @@ -322,7 +452,7 @@ public static bool ValidateSignature(string filePath, MinisignSignature signatur /// /// /// - public static bool ValidateSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + public static bool ValidateLegacySignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) { if (message == null) throw new ArgumentException("missing signature input", nameof(message)); @@ -335,17 +465,8 @@ public static bool ValidateSignature(byte[] message, MinisignSignature signature if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; - if (!signature.IsHashed) - { - // Legacy: Ed25519(message) - if (!PublicKeyAuth.VerifyDetached(signature.Signature, message, publicKey.PublicKey)) return false; - } - else - { - // Hashed: Ed25519(Blake2b-512(message)) - var blake = GenericHash.Hash(message, null, 64); - if (!PublicKeyAuth.VerifyDetached(signature.Signature, blake, publicKey.PublicKey)) return false; - } + // Legacy: Ed25519(message) + if (!PublicKeyAuth.VerifyDetached(signature.Signature, message, publicKey.PublicKey)) return false; return PublicKeyAuth.VerifyDetached( signature.GlobalSignature, @@ -353,7 +474,39 @@ public static bool ValidateSignature(byte[] message, MinisignSignature signature publicKey.PublicKey); } + /// + /// Validate a file with a MinisignSignature and a MinisignPublicKey object. + /// + /// The message to validate. + /// A valid MinisignSignature object. + /// A valid MinisignPublicKey object. + /// true if valid; otherwise, false. + /// + /// + /// + /// + public static bool ValidateHashedSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + { + if (message == null) + throw new ArgumentException("missing signature input", nameof(message)); + + if (signature == null) + throw new ArgumentException("missing signature input", nameof(signature)); + if (publicKey == null) + throw new ArgumentException("missing publicKey input", nameof(publicKey)); + + if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; + + // Hashed: Ed25519(Blake2b-512(message)) + var blake = GenericHash.Hash(message, null, 64); + if (!PublicKeyAuth.VerifyDetached(signature.Signature, blake, publicKey.PublicKey)) return false; + + return PublicKeyAuth.VerifyDetached( + signature.GlobalSignature, + ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), + publicKey.PublicKey); + } #endregion @@ -458,9 +611,8 @@ public static MinisignSignature LoadSignature(byte[] signature, byte[] trustedCo }; var alg = Encoding.UTF8.GetString(result.SignatureAlgorithm); - result.IsHashed = alg == "ED"; - if (!result.IsHashed) + if (alg == SigalgLegacy) { // Legacy minisign: Ed + keyid(8) + raw signature result.Signature = ArrayHelpers.SubArray(signature, 10); @@ -656,7 +808,7 @@ public static MinisignPrivateKey LoadPrivateKey(byte[] privateKey, byte[] passwo KdfMemLimit = BitConverter.ToInt64(ArrayHelpers.SubArray(privateKey, 46, 8), 0) //currently unused }; - if (!minisignPrivateKey.SignatureAlgorithm.SequenceEqual(Encoding.UTF8.GetBytes(Sigalg))) + if (!minisignPrivateKey.SignatureAlgorithm.SequenceEqual(Encoding.UTF8.GetBytes(SigalgLegacy))) { throw new CorruptPrivateKeyException("bad SignatureAlgorithm"); } diff --git a/Minisign.Net/Models/MinisignSignature.cs b/Minisign.Net/Models/MinisignSignature.cs index 387d7a3..a9b399f 100644 --- a/Minisign.Net/Models/MinisignSignature.cs +++ b/Minisign.Net/Models/MinisignSignature.cs @@ -7,6 +7,5 @@ public class MinisignSignature public byte[] Signature { get; set; } public byte[] GlobalSignature { get; set; } public byte[] TrustedComment { get; set; } - public bool IsHashed { get; set; } } } diff --git a/README.md b/README.md index 6a0c1b7..cf6c691 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,20 @@ Minisign.Net is a .NET port of [minisign](https://github.com/jedisct1/minisign) ### Sign a file ```csharp -public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") +public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") + +public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") ``` ### Validate a file ```csharp -public static bool ValidateSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) +public static bool ValidateHashedSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) + +public static bool ValidateHashedSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + +public static bool ValidateLegacySignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) -public static bool ValidateSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) +public static bool ValidateLegacySignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) ``` ### Generate a key pair From 2464301ff7089bf909d87e34467f7a8e47de6652 Mon Sep 17 00:00:00 2001 From: Max Raab Date: Sun, 16 Nov 2025 12:48:57 +0100 Subject: [PATCH 2/2] Set hashed signatures as default Set hashed signatures as default for signing and validating signatures. The validation process can recognize the signing method and selects the correct validation method. --- Minisign.Net.Tests/BaseTests.cs | 63 ++++++++-- Minisign.Net/Core.cs | 142 +++++++++++++++++++---- Minisign.Net/Models/MinisignSignature.cs | 1 + README.md | 8 +- 4 files changed, 179 insertions(+), 35 deletions(-) diff --git a/Minisign.Net.Tests/BaseTests.cs b/Minisign.Net.Tests/BaseTests.cs index 69b63a4..b4c6f12 100644 --- a/Minisign.Net.Tests/BaseTests.cs +++ b/Minisign.Net.Tests/BaseTests.cs @@ -27,7 +27,7 @@ public void GenerateKeyTest() } [Fact] - public void SignAndValidataLegacyTest() + public void SignAndValidateTest() { const string expected = "9d6f33b5e347042e"; const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; @@ -35,19 +35,19 @@ public void SignAndValidataLegacyTest() var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); var file = Path.Combine("Data", "testfile.jpg"); - var signedFile = Core.SignLegacy(file, minisignPrivateKey); + var signedFile = Core.Sign(file, minisignPrivateKey); var minisignSignature = Core.LoadSignatureFromFile(signedFile); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); - Assert.True(Core.ValidateLegacySignature(file, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateSignature(file, minisignSignature, minisignPublicKey)); File.Delete(signedFile); } [Fact] - public void SignAndValidateLegacy2Test() + public void SignAndValidate2Test() { const string expected = "9d6f33b5e347042e"; const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; @@ -56,19 +56,19 @@ public void SignAndValidateLegacy2Test() var file = Path.Combine("Data", "testfile.jpg"); var fileBinary = File.ReadAllBytes(file); - var signedFile = Core.SignLegacy(file, minisignPrivateKey); + var signedFile = Core.Sign(file, minisignPrivateKey); var minisignSignature = Core.LoadSignatureFromFile(signedFile); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); - Assert.True(Core.ValidateLegacySignature(fileBinary, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateSignature(fileBinary, minisignSignature, minisignPublicKey)); File.Delete(signedFile); } [Fact] - public void SignAndValidataHashedTest() + public void SignAndValidateHashedTest() { const string expected = "9d6f33b5e347042e"; const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; @@ -109,15 +109,44 @@ public void SignAndValidateHashed2Test() } [Fact] - public void ValidateLegacySignature() + public void SignAndValidateLegacyTest() { + const string expected = "9d6f33b5e347042e"; + const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; + const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; + var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); + var file = Path.Combine("Data", "testfile.jpg"); - var fileBinary = File.ReadAllBytes(file); + var signedFile = Core.SignLegacy(file, minisignPrivateKey); - var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig-legacy")); + var minisignSignature = Core.LoadSignatureFromFile(signedFile); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); + Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); + Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); Assert.True(Core.ValidateLegacySignature(file, minisignSignature, minisignPublicKey)); + File.Delete(signedFile); + } + + [Fact] + public void SignAndValidateLegacy2Test() + { + const string expected = "9d6f33b5e347042e"; + const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; + const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; + var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); + + var file = Path.Combine("Data", "testfile.jpg"); + var fileBinary = File.ReadAllBytes(file); + var signedFile = Core.SignLegacy(file, minisignPrivateKey); + + var minisignSignature = Core.LoadSignatureFromFile(signedFile); + var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); + Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); + Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); + + Assert.True(Core.ValidateLegacySignature(fileBinary, minisignSignature, minisignPublicKey)); + File.Delete(signedFile); } [Fact] @@ -129,7 +158,19 @@ public void ValidateHashedSignature() var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig")); var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); - Assert.True(Core.ValidateHashedSignature(file, minisignSignature, minisignPublicKey)); + Assert.True(Core.ValidateHashedSignature(fileBinary, minisignSignature, minisignPublicKey)); + } + + [Fact] + public void ValidateLegacySignature() + { + var file = Path.Combine("Data", "testfile.jpg"); + var fileBinary = File.ReadAllBytes(file); + + var minisignSignature = Core.LoadSignatureFromFile(Path.Combine("Data", "test.jpg.minisig-legacy")); + var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); + + Assert.True(Core.ValidateLegacySignature(file, minisignSignature, minisignPublicKey)); } [Fact] diff --git a/Minisign.Net/Core.cs b/Minisign.Net/Core.cs index 43e5abf..083558c 100644 --- a/Minisign.Net/Core.cs +++ b/Minisign.Net/Core.cs @@ -54,7 +54,33 @@ public static class Core /// /// /// - public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", + public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", + string trustedComment = "", string outputFolder = "") + { + return SignHashed(fileToSign, minisignPrivateKey, untrustedComment, trustedComment, outputFolder); + } + + /// + /// Sign a file with a MinisignPrivateKey. + /// + /// The full path to the file. + /// A valid MinisignPrivateKey to sign. + /// An optional untrusted comment. + /// An optional trusted comment. + /// The folder to write the signature (optional). + /// The full path to the signed file. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") { if (fileToSign != null && !File.Exists(fileToSign)) @@ -74,7 +100,7 @@ public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPr { var timestamp = GetTimestamp(); var filename = Path.GetFileName(fileToSign); - trustedComment = "timestamp: " + timestamp + " file: " + filename; + trustedComment = "timestamp: " + timestamp + " file: " + filename + " hashed"; } if ((CommentPrefix + untrustedComment).Length > CommentMaxBytes) @@ -101,12 +127,12 @@ public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPr if (outputFolder.IndexOfAny(Path.GetInvalidPathChars()) > -1) throw new ArgumentException("The given path to the output folder contains invalid characters!"); - var file = LoadMessageFile(fileToSign); + var file = ComputeBlake2bFileHash(fileToSign); var minisignSignature = new MinisignSignature { KeyId = minisignPrivateKey.KeyId, - SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgLegacy) + SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgHashed) }; var signature = PublicKeyAuth.SignDetached(file, minisignPrivateKey.SecretKey); minisignSignature.Signature = signature; @@ -157,7 +183,7 @@ public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPr /// /// /// - public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", + public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") { if (fileToSign != null && !File.Exists(fileToSign)) @@ -177,7 +203,7 @@ public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPr { var timestamp = GetTimestamp(); var filename = Path.GetFileName(fileToSign); - trustedComment = "timestamp: " + timestamp + " file: " + filename + " hashed"; + trustedComment = "timestamp: " + timestamp + " file: " + filename; } if ((CommentPrefix + untrustedComment).Length > CommentMaxBytes) @@ -204,12 +230,12 @@ public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPr if (outputFolder.IndexOfAny(Path.GetInvalidPathChars()) > -1) throw new ArgumentException("The given path to the output folder contains invalid characters!"); - var file = ComputeBlake2bFileHash(fileToSign); + var file = LoadMessageFile(fileToSign); var minisignSignature = new MinisignSignature { KeyId = minisignPrivateKey.KeyId, - SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgHashed) + SignatureAlgorithm = Encoding.UTF8.GetBytes(SigalgLegacy) }; var signature = PublicKeyAuth.SignDetached(file, minisignPrivateKey.SecretKey); minisignSignature.Signature = signature; @@ -379,7 +405,7 @@ public static MinisignKeyPair GenerateKeyPair(string password, bool writeOutputF /// /// /// - public static bool ValidateLegacySignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) + public static bool ValidateSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) { if (filePath != null && !File.Exists(filePath)) throw new FileNotFoundException("could not find filePath"); @@ -392,17 +418,48 @@ public static bool ValidateLegacySignature(string filePath, MinisignSignature si if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; + if (signature.IsHashed) + { + return ValidateHashedSignature(filePath, signature, publicKey); + } + else + { + return ValidateLegacySignature(filePath, signature, publicKey); + } + } - var file = LoadMessageFile(filePath); + /// + /// Validate a file with a MinisignSignature and a MinisignPublicKey object. + /// + /// The message to validate. + /// A valid MinisignSignature object. + /// A valid MinisignPublicKey object. + /// true if valid; otherwise, false. + /// + /// + /// + /// + public static bool ValidateSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + { + if (message == null) + throw new ArgumentException("missing signature input", nameof(message)); - // Legacy: Ed25519(message) - if (!PublicKeyAuth.VerifyDetached(signature.Signature, file, publicKey.PublicKey)) return false; + if (signature == null) + throw new ArgumentException("missing signature input", nameof(signature)); - // Global signature is the same for both formats - return PublicKeyAuth.VerifyDetached( - signature.GlobalSignature, - ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), - publicKey.PublicKey); + if (publicKey == null) + throw new ArgumentException("missing publicKey input", nameof(publicKey)); + + if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; + + if (signature.IsHashed) + { + return ValidateHashedSignature(message, signature, publicKey); + } + else + { + return ValidateLegacySignature(message, signature, publicKey); + } } /// @@ -452,7 +509,7 @@ public static bool ValidateHashedSignature(string filePath, MinisignSignature si /// /// /// - public static bool ValidateLegacySignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + public static bool ValidateHashedSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) { if (message == null) throw new ArgumentException("missing signature input", nameof(message)); @@ -465,9 +522,47 @@ public static bool ValidateLegacySignature(byte[] message, MinisignSignature sig if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; + // Hashed: Ed25519(Blake2b-512(message)) + var blake = GenericHash.Hash(message, null, 64); + if (!PublicKeyAuth.VerifyDetached(signature.Signature, blake, publicKey.PublicKey)) return false; + + return PublicKeyAuth.VerifyDetached( + signature.GlobalSignature, + ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), + publicKey.PublicKey); + } + + /// + /// Validate a file with a MinisignSignature and a MinisignPublicKey object. + /// + /// The full path to the file. + /// A valid MinisignSignature object. + /// A valid MinisignPublicKey object. + /// true if valid; otherwise, false. + /// + /// + /// + /// + /// + public static bool ValidateLegacySignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) + { + if (filePath != null && !File.Exists(filePath)) + throw new FileNotFoundException("could not find filePath"); + + if (signature == null) + throw new ArgumentException("missing signature input", nameof(signature)); + + if (publicKey == null) + throw new ArgumentException("missing publicKey input", nameof(publicKey)); + + if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; + + var file = LoadMessageFile(filePath); + // Legacy: Ed25519(message) - if (!PublicKeyAuth.VerifyDetached(signature.Signature, message, publicKey.PublicKey)) return false; + if (!PublicKeyAuth.VerifyDetached(signature.Signature, file, publicKey.PublicKey)) return false; + // Global signature is the same for both formats return PublicKeyAuth.VerifyDetached( signature.GlobalSignature, ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), @@ -485,7 +580,7 @@ public static bool ValidateLegacySignature(byte[] message, MinisignSignature sig /// /// /// - public static bool ValidateHashedSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + public static bool ValidateLegacySignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) { if (message == null) throw new ArgumentException("missing signature input", nameof(message)); @@ -498,9 +593,8 @@ public static bool ValidateHashedSignature(byte[] message, MinisignSignature sig if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; - // Hashed: Ed25519(Blake2b-512(message)) - var blake = GenericHash.Hash(message, null, 64); - if (!PublicKeyAuth.VerifyDetached(signature.Signature, blake, publicKey.PublicKey)) return false; + // Legacy: Ed25519(message) + if (!PublicKeyAuth.VerifyDetached(signature.Signature, message, publicKey.PublicKey)) return false; return PublicKeyAuth.VerifyDetached( signature.GlobalSignature, @@ -616,11 +710,13 @@ public static MinisignSignature LoadSignature(byte[] signature, byte[] trustedCo { // Legacy minisign: Ed + keyid(8) + raw signature result.Signature = ArrayHelpers.SubArray(signature, 10); + result.IsHashed = false; } else { // Hashed minisign: ED + keyid(8) + signature(64) result.Signature = ArrayHelpers.SubArray(signature, 10, 64); + result.IsHashed = true; } return result; diff --git a/Minisign.Net/Models/MinisignSignature.cs b/Minisign.Net/Models/MinisignSignature.cs index a9b399f..fb89d5c 100644 --- a/Minisign.Net/Models/MinisignSignature.cs +++ b/Minisign.Net/Models/MinisignSignature.cs @@ -7,5 +7,6 @@ public class MinisignSignature public byte[] Signature { get; set; } public byte[] GlobalSignature { get; set; } public byte[] TrustedComment { get; set; } + public bool IsHashed { get; set; } = true; } } diff --git a/README.md b/README.md index cf6c691..b483ba4 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,19 @@ Minisign.Net is a .NET port of [minisign](https://github.com/jedisct1/minisign) ### Sign a file ```csharp -public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") +public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") public static string SignHashed(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") + +public static string SignLegacy(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") ``` ### Validate a file ```csharp +public static bool ValidateSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) + +public static bool ValidateSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) + public static bool ValidateHashedSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) public static bool ValidateHashedSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey)