├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── dotnetcore.yml ├── .gitignore ├── LICENSE.md ├── Minisign.Net.Tests ├── BaseExceptionTests.cs ├── BaseTests.cs ├── Data │ ├── test.jpg.minisig │ ├── test.key │ ├── test.pub │ └── testfile.jpg ├── Minisign.Net.Tests.csproj └── xunit.runner.json ├── Minisign.Net.sln ├── Minisign.Net ├── Core.cs ├── Exceptions │ ├── CorruptPrivateKeyException.cs │ ├── CorruptPublicKeyException.cs │ ├── CorruptSignatureException.cs │ └── FileSizeExceededException.cs ├── Helper │ ├── ArrayHelpers.cs │ └── EncryptionHelpers.cs ├── Minisign.Net.csproj └── Models │ ├── MinisignKeyPair.cs │ ├── MinisignPrivateKey.cs │ ├── MinisignPublicKey.cs │ └── MinisignSignature.cs └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*.cs] 4 | indent_style = tab 5 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.101 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | - name: Test 25 | run: dotnet test --no-restore --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 - 2020 Christian Hermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Minisign.Net.Tests/BaseExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Minisign; 3 | using Xunit; 4 | 5 | namespace Tests 6 | { 7 | public class BaseExceptionTests 8 | { 9 | [Fact] 10 | public void GenerateKeyBaseFolderTest() 11 | { 12 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 13 | const string folder = "Test"; 14 | Assert.Throws( 15 | () => { Core.GenerateKeyPair(seckeypass, true, folder); }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Minisign.Net.Tests/BaseTests.cs: -------------------------------------------------------------------------------- 1 | using Minisign; 2 | using Sodium; 3 | using System.IO; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace Tests 8 | { 9 | public class BaseTests 10 | { 11 | [Fact] 12 | public void GenerateKeyTest() 13 | { 14 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 15 | 16 | var minisignKeyPair = Core.GenerateKeyPair(seckeypass, true, "Data"); 17 | Assert.True(File.Exists(minisignKeyPair.MinisignPrivateKeyFilePath)); 18 | Assert.True(File.Exists(minisignKeyPair.MinisignPublicKeyFilePath)); 19 | 20 | var minisignPrivateKey = Core.LoadPrivateKeyFromFile(minisignKeyPair.MinisignPrivateKeyFilePath, seckeypass); 21 | var minisignPublicKey = Core.LoadPublicKeyFromFile(minisignKeyPair.MinisignPublicKeyFilePath); 22 | 23 | Assert.Equal(minisignPublicKey.KeyId, minisignPrivateKey.KeyId); 24 | 25 | File.Delete(minisignKeyPair.MinisignPrivateKeyFilePath); 26 | File.Delete(minisignKeyPair.MinisignPublicKeyFilePath); 27 | } 28 | 29 | [Fact] 30 | public void SignTest() 31 | { 32 | const string expected = "9d6f33b5e347042e"; 33 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 34 | const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; 35 | var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); 36 | 37 | var file = Path.Combine("Data", "testfile.jpg"); 38 | var signedFile = Core.Sign(file, minisignPrivateKey); 39 | 40 | var minisignSignature = Core.LoadSignatureFromFile(signedFile); 41 | var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); 42 | Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); 43 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); 44 | 45 | Assert.True(Core.ValidateSignature(file, minisignSignature, minisignPublicKey)); 46 | File.Delete(signedFile); 47 | } 48 | 49 | [Fact] 50 | public void Sign2Test() 51 | { 52 | const string expected = "9d6f33b5e347042e"; 53 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 54 | const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; 55 | var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); 56 | 57 | var file = Path.Combine("Data", "testfile.jpg"); 58 | var fileBinary = File.ReadAllBytes(file); 59 | var signedFile = Core.Sign(file, minisignPrivateKey); 60 | 61 | var minisignSignature = Core.LoadSignatureFromFile(signedFile); 62 | var minisignPublicKey = Core.LoadPublicKeyFromFile(Path.Combine("Data", "test.pub")); 63 | Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); 64 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); 65 | 66 | Assert.True(Core.ValidateSignature(fileBinary, minisignSignature, minisignPublicKey)); 67 | File.Delete(signedFile); 68 | } 69 | 70 | 71 | [Fact] 72 | public void LoadSignatureFromStringTest() 73 | { 74 | const string expected = "9d6f33b5e347042e"; 75 | const string signatureString = "RWSdbzO140cELi+edKSQMZw/yrCDB3aetMNoPYsESNapZuUfHeE8JunmfFNykkZbXWRMy+0Y8aaONyhdGSZtbEXlw32RpDtMmgw="; 76 | const string trustedComment = "trusted comment: timestamp: 1439294334 file: testfile.jpg"; 77 | const string globalSignature = "sXw0VdGKvIgZibPYp9bR5jz01dRkBbWzEBFLpY/+u7MGwk4HJT/Kj8aB1iXW3w6n9/gSv33cd2sk7uDVFclIAA=="; 78 | var minisignSignature = Core.LoadSignatureFromString(signatureString, trustedComment, globalSignature); 79 | Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); 80 | } 81 | 82 | [Fact] 83 | public void LoadSignatureFromFileTest() 84 | { 85 | const string expected = "9d6f33b5e347042e"; 86 | var file = Path.Combine("Data", "test.jpg.minisig"); 87 | var minisignSignature = Core.LoadSignatureFromFile(file); 88 | Assert.Equal(expected, Utilities.BinaryToHex(minisignSignature.KeyId)); 89 | } 90 | 91 | [Fact] 92 | public void LoadPublicKeyFromStringTest() 93 | { 94 | const string expected = "9d6f33b5e347042e"; 95 | var minisignPublicKey = Core.LoadPublicKeyFromString("RWSdbzO140cELjh8lkBoBpp/UBg1pd9NgoPZF+y6ZSsEjavog68aNfMF"); 96 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); 97 | } 98 | 99 | [Fact] 100 | public void LoadPublicKeyFromFileTest() 101 | { 102 | const string expected = "9d6f33b5e347042e"; 103 | var file = Path.Combine("Data", "test.pub"); 104 | var minisignPublicKey = Core.LoadPublicKeyFromFile(file); 105 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPublicKey.KeyId)); 106 | } 107 | 108 | [Fact] 109 | public void LoadPrivateKeyTest() 110 | { 111 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 112 | const string expected = 113 | "521437eb06d390e3881d6227543c670bd79ce4092845a4d567e85013c6ffe454387c964068069a7f501835a5df4d8283d917ecba652b048dabe883af1a35f305"; 114 | const string privateKey = "456453634232aeb543fbea3467ad996ac237b38646bcbc12e6232fbc0a8cd9a1ed46c7263af200000002000000000000004000000000992f22d875591d3bb7dc3f77caba3229e2f7b8afe655140bafabcb6c5d8b259366a2897624de65743de71f8f2dcc545a96c4b530ffd796d92f35eb02425f4196ab9a37ff2f542774d676625f8de689fa2da3e0a0250efd58347c35b927ca49ec4d93687be59d6e1a"; 115 | var minisignPrivateKey = Core.LoadPrivateKey(Utilities.HexToBinary(privateKey), Encoding.UTF8.GetBytes(seckeypass)); 116 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPrivateKey.SecretKey)); 117 | } 118 | 119 | [Fact] 120 | public void LoadPrivateKeyFromStringTest() 121 | { 122 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 123 | const string expected = 124 | "521437eb06d390e3881d6227543c670bd79ce4092845a4d567e85013c6ffe454387c964068069a7f501835a5df4d8283d917ecba652b048dabe883af1a35f305"; 125 | var minisignPrivateKey = Core.LoadPrivateKeyFromString("RWRTY0IyrrVD++o0Z62ZasI3s4ZGvLwS5iMvvAqM2aHtRscmOvIAAAACAAAAAAAAAEAAAAAAmS8i2HVZHTu33D93yroyKeL3uK/mVRQLr6vLbF2LJZNmool2JN5ldD3nH48tzFRalsS1MP/XltkvNesCQl9BlquaN/8vVCd01nZiX43mifoto+CgJQ79WDR8NbknyknsTZNoe+Wdbho=", seckeypass); 126 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPrivateKey.SecretKey)); 127 | } 128 | 129 | [Fact] 130 | public void LoadPrivateKeyFromFileTest() 131 | { 132 | const string seckeypass = "7e725ac9f52336f74dc54bbe2912855f79baacc08b008437809fq5527f1b2256"; 133 | const string expected = 134 | "521437eb06d390e3881d6227543c670bd79ce4092845a4d567e85013c6ffe454387c964068069a7f501835a5df4d8283d917ecba652b048dabe883af1a35f305"; 135 | var file = Path.Combine("Data", "test.key"); 136 | var minisignPrivateKey = Core.LoadPrivateKeyFromFile(file, seckeypass); 137 | Assert.Equal(expected, Utilities.BinaryToHex(minisignPrivateKey.SecretKey)); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Minisign.Net.Tests/Data/test.jpg.minisig: -------------------------------------------------------------------------------- 1 | untrusted comment: signature from minisign secret key 2 | RWSdbzO140cELi+edKSQMZw/yrCDB3aetMNoPYsESNapZuUfHeE8JunmfFNykkZbXWRMy+0Y8aaONyhdGSZtbEXlw32RpDtMmgw= 3 | trusted comment: timestamp: 1439294334 file: testfile.jpg 4 | sXw0VdGKvIgZibPYp9bR5jz01dRkBbWzEBFLpY/+u7MGwk4HJT/Kj8aB1iXW3w6n9/gSv33cd2sk7uDVFclIAA== 5 | -------------------------------------------------------------------------------- /Minisign.Net.Tests/Data/test.key: -------------------------------------------------------------------------------- 1 | untrusted comment: minisign encrypted secret key 2 | RWRTY0IyrrVD++o0Z62ZasI3s4ZGvLwS5iMvvAqM2aHtRscmOvIAAAACAAAAAAAAAEAAAAAAmS8i2HVZHTu33D93yroyKeL3uK/mVRQLr6vLbF2LJZNmool2JN5ldD3nH48tzFRalsS1MP/XltkvNesCQl9BlquaN/8vVCd01nZiX43mifoto+CgJQ79WDR8NbknyknsTZNoe+Wdbho= 3 | -------------------------------------------------------------------------------- /Minisign.Net.Tests/Data/test.pub: -------------------------------------------------------------------------------- 1 | untrusted comment: minisign public key 9D6F33B5E347042E 2 | RWSdbzO140cELjh8lkBoBpp/UBg1pd9NgoPZF+y6ZSsEjavog68aNfMF 3 | -------------------------------------------------------------------------------- /Minisign.Net.Tests/Data/testfile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbeans/minisign-net/7df7d878d615e46dfc88fbe30f7fe9b12f457c85/Minisign.Net.Tests/Data/testfile.jpg -------------------------------------------------------------------------------- /Minisign.Net.Tests/Minisign.Net.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | Tests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Minisign.Net.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "shadowCopy": false 3 | } -------------------------------------------------------------------------------- /Minisign.Net.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29920.165 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minisign.Net", "Minisign.Net\Minisign.Net.csproj", "{D9E98B5B-901C-4898-8BB0-6FCDF83C662A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minisign.Net.Tests", "Minisign.Net.Tests\Minisign.Net.Tests.csproj", "{D2A51C27-F286-45A2-A3B9-557B06D73355}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{87D6D941-F38E-46AC-9C32-E343FE41C382}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {D9E98B5B-901C-4898-8BB0-6FCDF83C662A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {D9E98B5B-901C-4898-8BB0-6FCDF83C662A}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {D9E98B5B-901C-4898-8BB0-6FCDF83C662A}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D9E98B5B-901C-4898-8BB0-6FCDF83C662A}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {D2A51C27-F286-45A2-A3B9-557B06D73355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {D2A51C27-F286-45A2-A3B9-557B06D73355}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {D2A51C27-F286-45A2-A3B9-557B06D73355}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {D2A51C27-F286-45A2-A3B9-557B06D73355}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {4BD304BC-C37E-43A1-A2E8-7AE252DAE641} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /Minisign.Net/Core.cs: -------------------------------------------------------------------------------- 1 | using Minisign.Exceptions; 2 | using Minisign.Helper; 3 | using Minisign.Models; 4 | using Sodium; 5 | using System; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Security; 9 | using System.Text; 10 | 11 | namespace Minisign 12 | { 13 | /// 14 | /// Main class to handle minisign files and objects. 15 | /// 16 | public static class Core 17 | { 18 | private const long MaxMessageFileSize = 1024000000; 19 | private const int CommentMaxBytes = 1024; 20 | private const int TrustedCommentMaxBytes = 8192; 21 | private const int KeyNumBytes = 8; 22 | private const int KeySaltBytes = 32; 23 | private const string Sigalg = "Ed"; 24 | private const string Kdfalg = "Sc"; 25 | private const string Chkalg = "B2"; 26 | private const string DefaultComment = "signature from minisign secret key"; 27 | private const string CommentPrefix = "untrusted comment: "; 28 | private const string TrustedCommentPrefix = "trusted comment: "; 29 | private const string PrivateKeyDefaultComment = "minisign encrypted secret key"; 30 | private const string SigSuffix = ".minisig"; 31 | private const string PrivateKeyFileSuffix = ".key"; 32 | private const string PublicKeyFileSuffix = ".pub"; 33 | 34 | #region Main Functions 35 | 36 | /// 37 | /// Sign a file with a MinisignPrivateKey. 38 | /// 39 | /// The full path to the file. 40 | /// A valid MinisignPrivateKey to sign. 41 | /// An optional untrusted comment. 42 | /// An optional trusted comment. 43 | /// The folder to write the signature (optional). 44 | /// The full path to the signed file. 45 | /// 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", 57 | string trustedComment = "", string outputFolder = "") 58 | { 59 | if (fileToSign != null && !File.Exists(fileToSign)) 60 | { 61 | throw new FileNotFoundException("could not find fileToSign"); 62 | } 63 | 64 | if (minisignPrivateKey == null) 65 | throw new ArgumentException("missing minisignPrivateKey input", nameof(minisignPrivateKey)); 66 | 67 | if (string.IsNullOrEmpty(untrustedComment)) 68 | { 69 | untrustedComment = DefaultComment; 70 | } 71 | 72 | if (string.IsNullOrEmpty(trustedComment)) 73 | { 74 | var timestamp = GetTimestamp(); 75 | var filename = Path.GetFileName(fileToSign); 76 | trustedComment = "timestamp: " + timestamp + " file: " + filename; 77 | } 78 | 79 | if ((CommentPrefix + untrustedComment).Length > CommentMaxBytes) 80 | { 81 | throw new ArgumentOutOfRangeException(nameof(untrustedComment), "untrustedComment too long"); 82 | } 83 | 84 | if ((TrustedCommentPrefix + trustedComment).Length > TrustedCommentMaxBytes) 85 | { 86 | throw new ArgumentOutOfRangeException(nameof(trustedComment), "trustedComment too long"); 87 | } 88 | 89 | if (string.IsNullOrEmpty(outputFolder)) 90 | { 91 | outputFolder = Path.GetDirectoryName(fileToSign); 92 | } 93 | 94 | //validate the outputFolder 95 | if (string.IsNullOrEmpty(outputFolder) || !Directory.Exists(outputFolder)) 96 | { 97 | throw new DirectoryNotFoundException("outputFolder must exist"); 98 | } 99 | 100 | if (outputFolder.IndexOfAny(Path.GetInvalidPathChars()) > -1) 101 | throw new ArgumentException("The given path to the output folder contains invalid characters!"); 102 | 103 | var file = LoadMessageFile(fileToSign); 104 | 105 | var minisignSignature = new MinisignSignature 106 | { 107 | KeyId = minisignPrivateKey.KeyId, 108 | SignatureAlgorithm = Encoding.UTF8.GetBytes(Sigalg) 109 | }; 110 | var signature = PublicKeyAuth.SignDetached(file, minisignPrivateKey.SecretKey); 111 | minisignSignature.Signature = signature; 112 | 113 | var binarySignature = ArrayHelpers.ConcatArrays( 114 | minisignSignature.SignatureAlgorithm, 115 | minisignSignature.KeyId, 116 | minisignSignature.Signature 117 | ); 118 | 119 | // sign the signature and the trusted comment with a global signature 120 | var globalSignature = 121 | PublicKeyAuth.SignDetached( 122 | ArrayHelpers.ConcatArrays(minisignSignature.Signature, Encoding.UTF8.GetBytes(trustedComment)), 123 | minisignPrivateKey.SecretKey); 124 | 125 | // prepare the file lines 126 | var signatureFileContent = new[] 127 | { 128 | CommentPrefix + untrustedComment, 129 | Convert.ToBase64String(binarySignature), 130 | TrustedCommentPrefix + trustedComment, 131 | Convert.ToBase64String(globalSignature) 132 | }; 133 | 134 | var outputFile = fileToSign + SigSuffix; 135 | File.WriteAllLines(outputFile, signatureFileContent); 136 | return outputFile; 137 | } 138 | 139 | /// 140 | /// Generate a new Minisign key pair. 141 | /// 142 | /// The password to protect the secret key. 143 | /// If false, no files will be written. 144 | /// The folder to write the files (optional). 145 | /// The name of the files to write (optional). 146 | /// A MinisignKeyPair object. 147 | /// 148 | /// 149 | /// 150 | /// 151 | /// 152 | /// 153 | /// 154 | /// 155 | /// 156 | /// 157 | public static MinisignKeyPair GenerateKeyPair(string password, bool writeOutputFiles = false, 158 | string outputFolder = "", string keyPairFileName = "minisign") 159 | { 160 | if (string.IsNullOrEmpty(password)) 161 | { 162 | throw new ArgumentNullException(nameof(password), "password can not be null"); 163 | } 164 | 165 | if (writeOutputFiles) 166 | { 167 | //validate the outputFolder 168 | if (string.IsNullOrEmpty(outputFolder) || !Directory.Exists(outputFolder)) 169 | { 170 | throw new DirectoryNotFoundException("outputFolder must exist"); 171 | } 172 | 173 | if (outputFolder.IndexOfAny(Path.GetInvalidPathChars()) > -1) 174 | throw new ArgumentException("The given path to the output folder contains invalid characters!"); 175 | 176 | //validate the keyPairFileName 177 | if (string.IsNullOrEmpty(keyPairFileName)) 178 | { 179 | throw new ArgumentNullException(nameof(keyPairFileName), "keyPairFileName can not be empty"); 180 | } 181 | } 182 | 183 | var minisignKeyPair = new MinisignKeyPair(); 184 | var minisignPrivateKey = new MinisignPrivateKey(); 185 | var keyPair = PublicKeyAuth.GenerateKeyPair(); 186 | var keyId = SodiumCore.GetRandomBytes(KeyNumBytes); 187 | var kdfSalt = SodiumCore.GetRandomBytes(32); 188 | 189 | minisignPrivateKey.PublicKey = keyPair.PublicKey; 190 | minisignPrivateKey.KdfSalt = kdfSalt; 191 | minisignPrivateKey.SignatureAlgorithm = Encoding.UTF8.GetBytes(Sigalg); 192 | minisignPrivateKey.ChecksumAlgorithm = Encoding.UTF8.GetBytes(Chkalg); 193 | minisignPrivateKey.KdfAlgorithm = Encoding.UTF8.GetBytes(Kdfalg); 194 | minisignPrivateKey.KdfMemLimit = 1073741824; //currently unused 195 | minisignPrivateKey.KdfOpsLimit = 33554432; //currently unused 196 | 197 | var checksum = 198 | GenericHash.Hash( 199 | ArrayHelpers.ConcatArrays(minisignPrivateKey.SignatureAlgorithm, keyId, keyPair.PrivateKey), null, 200 | 32); 201 | minisignPrivateKey.KeyId = keyId; 202 | minisignPrivateKey.SecretKey = keyPair.PrivateKey; 203 | minisignPrivateKey.Checksum = checksum; 204 | 205 | var dataToProtect = ArrayHelpers.ConcatArrays(keyId, keyPair.PrivateKey, checksum); 206 | var encryptionKey = PasswordHash.ScryptHashBinary(Encoding.UTF8.GetBytes(password), 207 | minisignPrivateKey.KdfSalt, 208 | PasswordHash.Strength.Sensitive, 209 | 104); 210 | 211 | var encryptedKeyData = EncryptionHelpers.Xor(dataToProtect, encryptionKey); 212 | // set up the public key 213 | var minisignPublicKey = new MinisignPublicKey 214 | { 215 | KeyId = keyId, 216 | PublicKey = keyPair.PublicKey, 217 | SignatureAlgorithm = Encoding.UTF8.GetBytes(Sigalg) 218 | }; 219 | keyPair.Dispose(); 220 | if (writeOutputFiles) 221 | { 222 | var privateKeyOutputFileName = Path.Combine(outputFolder, keyPairFileName + PrivateKeyFileSuffix); 223 | var publicKeyOutputFileName = Path.Combine(outputFolder, keyPairFileName + PublicKeyFileSuffix); 224 | 225 | var binaryPublicKey = ArrayHelpers.ConcatArrays( 226 | minisignPublicKey.SignatureAlgorithm, 227 | minisignPublicKey.KeyId, 228 | minisignPublicKey.PublicKey 229 | ); 230 | 231 | var publicFileContent = new[] 232 | { 233 | CommentPrefix + "minisign public key " + 234 | Utilities.BinaryToHex(minisignPublicKey.KeyId, Utilities.HexFormat.None, Utilities.HexCase.Upper), 235 | Convert.ToBase64String(binaryPublicKey) 236 | }; 237 | 238 | var binaryPrivateKey = ArrayHelpers.ConcatArrays( 239 | minisignPrivateKey.SignatureAlgorithm, 240 | minisignPrivateKey.KdfAlgorithm, 241 | minisignPrivateKey.ChecksumAlgorithm, 242 | minisignPrivateKey.KdfSalt, 243 | BitConverter.GetBytes(minisignPrivateKey.KdfOpsLimit), 244 | BitConverter.GetBytes(minisignPrivateKey.KdfMemLimit), 245 | encryptedKeyData 246 | ); 247 | 248 | var privateFileContent = new[] 249 | { 250 | CommentPrefix + PrivateKeyDefaultComment, 251 | Convert.ToBase64String(binaryPrivateKey) 252 | }; 253 | // files will be overwritten! 254 | File.WriteAllLines(publicKeyOutputFileName, publicFileContent); 255 | File.WriteAllLines(privateKeyOutputFileName, privateFileContent); 256 | 257 | minisignKeyPair.MinisignPublicKeyFilePath = publicKeyOutputFileName; 258 | minisignKeyPair.MinisignPrivateKeyFilePath = privateKeyOutputFileName; 259 | } 260 | 261 | minisignKeyPair.MinisignPublicKey = minisignPublicKey; 262 | minisignKeyPair.MinisignPrivateKey = minisignPrivateKey; 263 | return minisignKeyPair; 264 | } 265 | 266 | /// 267 | /// Validate a file with a MinisignSignature and a MinisignPublicKey object. 268 | /// 269 | /// The full path to the file. 270 | /// A valid MinisignSignature object. 271 | /// A valid MinisignPublicKey object. 272 | /// true if valid; otherwise, false. 273 | /// 274 | /// 275 | /// 276 | /// 277 | /// 278 | public static bool ValidateSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) 279 | { 280 | if (filePath != null && !File.Exists(filePath)) 281 | { 282 | throw new FileNotFoundException("could not find filePath"); 283 | } 284 | 285 | if (signature == null) 286 | throw new ArgumentException("missing signature input", nameof(signature)); 287 | 288 | if (publicKey == null) 289 | throw new ArgumentException("missing publicKey input", nameof(publicKey)); 290 | 291 | if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; 292 | // load the file into memory 293 | var file = LoadMessageFile(filePath); 294 | // verify the signature 295 | if (PublicKeyAuth.VerifyDetached(signature.Signature, file, publicKey.PublicKey)) 296 | { 297 | // verify the trusted comment 298 | return PublicKeyAuth.VerifyDetached(signature.GlobalSignature, 299 | ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), publicKey.PublicKey); 300 | } 301 | 302 | return false; 303 | } 304 | 305 | /// 306 | /// Validate a file with a MinisignSignature and a MinisignPublicKey object. 307 | /// 308 | /// The message to validate. 309 | /// A valid MinisignSignature object. 310 | /// A valid MinisignPublicKey object. 311 | /// true if valid; otherwise, false. 312 | /// 313 | /// 314 | /// 315 | /// 316 | public static bool ValidateSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) 317 | { 318 | if (message == null) 319 | throw new ArgumentException("missing signature input", nameof(message)); 320 | 321 | if (signature == null) 322 | throw new ArgumentException("missing signature input", nameof(signature)); 323 | 324 | if (publicKey == null) 325 | throw new ArgumentException("missing publicKey input", nameof(publicKey)); 326 | 327 | if (!ArrayHelpers.ConstantTimeEquals(signature.KeyId, publicKey.KeyId)) return false; 328 | // verify the signature 329 | if (PublicKeyAuth.VerifyDetached(signature.Signature, message, publicKey.PublicKey)) 330 | { 331 | // verify the trusted comment 332 | return PublicKeyAuth.VerifyDetached(signature.GlobalSignature, 333 | ArrayHelpers.ConcatArrays(signature.Signature, signature.TrustedComment), publicKey.PublicKey); 334 | } 335 | 336 | return false; 337 | } 338 | 339 | #endregion 340 | 341 | #region Signature Handling 342 | 343 | /// 344 | /// Load a signature from strings into a MinisignSignature object. 345 | /// 346 | /// A valid base64 signature string. 347 | /// The associated trusted comment. 348 | /// The associated base64 global signature string. 349 | /// A MinisignSignature object. 350 | /// 351 | /// 352 | /// 353 | /// 354 | public static MinisignSignature LoadSignatureFromString(string signatureString, string trustedComment, 355 | string globalSignature) 356 | { 357 | if (string.IsNullOrEmpty(signatureString)) 358 | throw new ArgumentException("signatureString can not be null", nameof(signatureString)); 359 | 360 | if (string.IsNullOrEmpty(trustedComment)) 361 | throw new ArgumentException("trustedComment can not be null", nameof(trustedComment)); 362 | 363 | if (string.IsNullOrEmpty(globalSignature)) 364 | throw new ArgumentException("globalSignature can not be null", nameof(globalSignature)); 365 | 366 | return LoadSignature(Convert.FromBase64String(signatureString), Encoding.UTF8.GetBytes(trustedComment), 367 | Convert.FromBase64String(globalSignature)); 368 | } 369 | 370 | /// 371 | /// Load a signature from a file into a MinisignSignature object. 372 | /// 373 | /// The full path to the signature file. 374 | /// A MinisignSignature object. 375 | /// 376 | /// 377 | /// 378 | /// 379 | /// 380 | /// 381 | /// 382 | /// 383 | /// 384 | /// 385 | /// 386 | public static MinisignSignature LoadSignatureFromFile(string signatureFile) 387 | { 388 | if (signatureFile != null && !File.Exists(signatureFile)) 389 | { 390 | throw new FileNotFoundException("could not find signatureFile"); 391 | } 392 | 393 | var signatureLines = File.ReadLines(signatureFile).Take(4).ToList(); 394 | if (signatureLines.Count != 4) 395 | { 396 | throw new CorruptSignatureException("the signature file has missing lines"); 397 | } 398 | 399 | // do some simple pre-validation 400 | if (!signatureLines[0].StartsWith(CommentPrefix) && 401 | !signatureLines[2].StartsWith(TrustedCommentPrefix)) 402 | { 403 | throw new CorruptSignatureException("the signature file has invalid lines"); 404 | } 405 | var trimmedComment = signatureLines[2].Replace(TrustedCommentPrefix, "").Trim(); 406 | var trustedCommentBinary = Encoding.UTF8.GetBytes(trimmedComment); 407 | return LoadSignature(Convert.FromBase64String(signatureLines[1].Trim()), trustedCommentBinary, 408 | Convert.FromBase64String(signatureLines[3].Trim())); 409 | } 410 | 411 | /// 412 | /// Load a signature into a MinisignSignature object. 413 | /// 414 | /// A valid signature. 415 | /// The associated trustedComment. 416 | /// The associated globalSignature. 417 | /// A MinisignSignature object. 418 | /// 419 | /// 420 | /// 421 | /// 422 | public static MinisignSignature LoadSignature(byte[] signature, byte[] trustedComment, byte[] globalSignature) 423 | { 424 | if (signature == null) 425 | throw new ArgumentException("missing signature input", nameof(signature)); 426 | 427 | if (trustedComment == null) 428 | throw new ArgumentException("missing trustedComment input", nameof(trustedComment)); 429 | 430 | if (globalSignature == null) 431 | throw new ArgumentException("missing globalSignature input", nameof(globalSignature)); 432 | 433 | var minisignSignature = new MinisignSignature 434 | { 435 | SignatureAlgorithm = ArrayHelpers.SubArray(signature, 0, 2), 436 | KeyId = ArrayHelpers.SubArray(signature, 2, 8), 437 | Signature = ArrayHelpers.SubArray(signature, 10), 438 | TrustedComment = trustedComment, 439 | GlobalSignature = globalSignature 440 | }; 441 | return minisignSignature; 442 | } 443 | 444 | #endregion 445 | 446 | #region Public Key Handling 447 | 448 | /// 449 | /// Load a public key from a string into a MinisignPublicKey object. 450 | /// 451 | /// A valid base64 public key string. 452 | /// A MinisignPublicKey object. 453 | /// 454 | /// 455 | /// 456 | /// 457 | public static MinisignPublicKey LoadPublicKeyFromString(string publicKeyString) 458 | { 459 | if (string.IsNullOrEmpty(publicKeyString)) 460 | throw new ArgumentException("publicKeyString can not be null", nameof(publicKeyString)); 461 | 462 | return LoadPublicKey(Convert.FromBase64String(publicKeyString)); 463 | } 464 | 465 | /// 466 | /// Load a public key from a file into a MinisignPublicKey object. 467 | /// 468 | /// The full path to the public key file. 469 | /// A MinisignPublicKey object. 470 | /// 471 | /// 472 | /// 473 | /// 474 | /// 475 | /// 476 | /// 477 | /// 478 | /// 479 | /// 480 | /// 481 | public static MinisignPublicKey LoadPublicKeyFromFile(string publicKeyFile) 482 | { 483 | if (publicKeyFile != null && !File.Exists(publicKeyFile)) 484 | { 485 | throw new FileNotFoundException("could not find publicKeyFile"); 486 | } 487 | 488 | var publicKeyLines = File.ReadLines(publicKeyFile).Take(2).ToList(); 489 | if (publicKeyLines.Count != 2) 490 | { 491 | throw new CorruptPublicKeyException("the public key file has missing lines"); 492 | } 493 | 494 | // do some simple pre-validation 495 | if (!publicKeyLines[0].StartsWith(CommentPrefix)) 496 | { 497 | throw new CorruptPublicKeyException("the public key file has invalid lines"); 498 | } 499 | 500 | return LoadPublicKey(Convert.FromBase64String(publicKeyLines[1])); 501 | } 502 | 503 | /// 504 | /// Load a public key into a MinisignPublicKey object. 505 | /// 506 | /// A valid public key. 507 | /// A MinisignPublicKey object. 508 | /// 509 | /// 510 | /// 511 | /// 512 | public static MinisignPublicKey LoadPublicKey(byte[] publicKey) 513 | { 514 | if (publicKey == null) 515 | throw new ArgumentException("missing publicKey input", nameof(publicKey)); 516 | 517 | var minisignPublicKey = new MinisignPublicKey 518 | { 519 | SignatureAlgorithm = ArrayHelpers.SubArray(publicKey, 0, 2), 520 | KeyId = ArrayHelpers.SubArray(publicKey, 2, 8), 521 | PublicKey = ArrayHelpers.SubArray(publicKey, 10) 522 | }; 523 | 524 | return minisignPublicKey; 525 | } 526 | 527 | #endregion 528 | 529 | #region Private Key Handling 530 | 531 | /// 532 | /// Load a private key from a string into a MinisignPrivateKey object. 533 | /// 534 | /// A valid Base64 string. 535 | /// The password to decrypt the private key. 536 | /// A MinisignPrivateKey object. 537 | /// 538 | /// 539 | /// 540 | /// 541 | /// 542 | public static MinisignPrivateKey LoadPrivateKeyFromString(string privateKeyString, string password) 543 | { 544 | if (string.IsNullOrEmpty(privateKeyString)) 545 | throw new ArgumentException("privateKeyString can not be null", nameof(privateKeyString)); 546 | 547 | if (string.IsNullOrEmpty(password)) 548 | throw new ArgumentException("password can not be null", nameof(password)); 549 | 550 | return LoadPrivateKey(Convert.FromBase64String(privateKeyString), Encoding.UTF8.GetBytes(password)); 551 | } 552 | 553 | /// 554 | /// Load a private key from a file into a MinisignPrivateKey object. 555 | /// 556 | /// The full path to to the private key file. 557 | /// The password to decrypt the private key. 558 | /// A MinisignPrivateKey object. 559 | /// 560 | /// 561 | /// 562 | /// 563 | /// 564 | /// 565 | /// 566 | /// 567 | /// 568 | /// 569 | /// 570 | public static MinisignPrivateKey LoadPrivateKeyFromFile(string privateKeyFile, string password) 571 | { 572 | if (privateKeyFile != null && !File.Exists(privateKeyFile)) 573 | { 574 | throw new FileNotFoundException("could not find privateKeyFile"); 575 | } 576 | 577 | if (string.IsNullOrEmpty(password)) 578 | { 579 | throw new ArgumentException("password can not be null", nameof(password)); 580 | } 581 | 582 | var privateKeyLines = File.ReadLines(privateKeyFile).Take(2).ToList(); 583 | if (privateKeyLines.Count != 2) 584 | { 585 | throw new CorruptPrivateKeyException("the private key file has missing lines"); 586 | } 587 | 588 | // do some simple pre-validation 589 | if (!privateKeyLines[0].StartsWith(CommentPrefix)) 590 | { 591 | throw new CorruptPrivateKeyException("the private key file has invalid lines"); 592 | } 593 | 594 | return LoadPrivateKey(Convert.FromBase64String(privateKeyLines[1]), Encoding.UTF8.GetBytes(password)); 595 | } 596 | 597 | /// 598 | /// Load a public key into a MinisignPublicKey object. 599 | /// 600 | /// A valid private key. 601 | /// The password to decrypt the private key. 602 | /// A MinisignPrivateKey object. 603 | /// 604 | /// 605 | /// 606 | /// 607 | /// 608 | public static MinisignPrivateKey LoadPrivateKey(byte[] privateKey, byte[] password) 609 | { 610 | if (privateKey == null) 611 | throw new ArgumentException("missing privateKey input", nameof(privateKey)); 612 | 613 | if (password == null) 614 | throw new ArgumentException("missing password input", nameof(password)); 615 | 616 | var minisignPrivateKey = new MinisignPrivateKey 617 | { 618 | SignatureAlgorithm = ArrayHelpers.SubArray(privateKey, 0, 2), 619 | KdfAlgorithm = ArrayHelpers.SubArray(privateKey, 2, 2), 620 | ChecksumAlgorithm = ArrayHelpers.SubArray(privateKey, 4, 2), 621 | KdfSalt = ArrayHelpers.SubArray(privateKey, 6, 32), 622 | KdfOpsLimit = BitConverter.ToInt64(ArrayHelpers.SubArray(privateKey, 38, 8), 0), //currently unused 623 | KdfMemLimit = BitConverter.ToInt64(ArrayHelpers.SubArray(privateKey, 46, 8), 0) //currently unused 624 | }; 625 | 626 | if (!minisignPrivateKey.SignatureAlgorithm.SequenceEqual(Encoding.UTF8.GetBytes(Sigalg))) 627 | { 628 | throw new CorruptPrivateKeyException("bad SignatureAlgorithm"); 629 | } 630 | 631 | if (!minisignPrivateKey.ChecksumAlgorithm.SequenceEqual(Encoding.UTF8.GetBytes(Chkalg))) 632 | { 633 | throw new CorruptPrivateKeyException("bad ChecksumAlgorithm"); 634 | } 635 | 636 | if (!minisignPrivateKey.KdfAlgorithm.SequenceEqual(Encoding.UTF8.GetBytes(Kdfalg))) 637 | { 638 | throw new CorruptPrivateKeyException("bad KdfAlgorithm"); 639 | } 640 | 641 | if (minisignPrivateKey.KdfSalt.Length != KeySaltBytes) 642 | { 643 | throw new CorruptPrivateKeyException("bad KdfSalt length"); 644 | } 645 | 646 | var encryptedKeyData = ArrayHelpers.SubArray(privateKey, 54, 104); 647 | 648 | var decryptionKey = PasswordHash.ScryptHashBinary(password, minisignPrivateKey.KdfSalt, 649 | PasswordHash.Strength.Sensitive, 650 | 104); 651 | 652 | var decryptedKeyData = EncryptionHelpers.Xor(encryptedKeyData, decryptionKey); 653 | minisignPrivateKey.KeyId = ArrayHelpers.SubArray(decryptedKeyData, 0, 8); 654 | minisignPrivateKey.SecretKey = ArrayHelpers.SubArray(decryptedKeyData, 8, 64); 655 | minisignPrivateKey.Checksum = ArrayHelpers.SubArray(decryptedKeyData, 72, 32); 656 | 657 | if (minisignPrivateKey.KeyId.Length != KeyNumBytes) 658 | { 659 | throw new CorruptPrivateKeyException("bad KeyId length"); 660 | } 661 | 662 | var calculatedChecksum = 663 | GenericHash.Hash( 664 | ArrayHelpers.ConcatArrays(minisignPrivateKey.SignatureAlgorithm, minisignPrivateKey.KeyId, 665 | minisignPrivateKey.SecretKey), null, 32); 666 | 667 | if (!ArrayHelpers.ConstantTimeEquals(minisignPrivateKey.Checksum, calculatedChecksum)) 668 | { 669 | throw new CorruptPrivateKeyException("bad private key checksum"); 670 | } 671 | // extract the public key from the private key 672 | minisignPrivateKey.PublicKey = 673 | PublicKeyAuth.ExtractEd25519PublicKeyFromEd25519SecretKey(minisignPrivateKey.SecretKey); 674 | 675 | return minisignPrivateKey; 676 | } 677 | 678 | #endregion 679 | 680 | #region Helper 681 | 682 | /// 683 | /// Loads a file into memory. 684 | /// 685 | /// Path to the file. 686 | /// The file as byte array. 687 | /// 688 | /// 689 | /// 690 | /// 691 | /// 692 | /// 693 | /// 694 | private static byte[] LoadMessageFile(string messageFile) 695 | { 696 | if (messageFile == null) 697 | throw new ArgumentException("missing messageFile input", nameof(messageFile)); 698 | 699 | if (!File.Exists(messageFile)) 700 | { 701 | throw new FileNotFoundException("could not find messageFile"); 702 | } 703 | 704 | var messageFileLength = new FileInfo(messageFile); 705 | if (messageFileLength.Length >= MaxMessageFileSize) 706 | { 707 | throw new FileSizeExceededException("data has to be smaller than 1 Gb"); 708 | } 709 | 710 | return File.ReadAllBytes(messageFile); 711 | } 712 | 713 | /// 714 | /// Get the current Unix Timestamp. 715 | /// 716 | /// The current Unix Timestamp. 717 | private static int GetTimestamp() 718 | { 719 | return (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; 720 | } 721 | 722 | #endregion 723 | } 724 | } 725 | -------------------------------------------------------------------------------- /Minisign.Net/Exceptions/CorruptPrivateKeyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minisign.Exceptions 4 | { 5 | public class CorruptPrivateKeyException : Exception 6 | { 7 | public CorruptPrivateKeyException() 8 | { 9 | } 10 | 11 | public CorruptPrivateKeyException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public CorruptPrivateKeyException(string message, Exception inner) 17 | : base(message, inner) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Minisign.Net/Exceptions/CorruptPublicKeyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minisign.Exceptions 4 | { 5 | public class CorruptPublicKeyException : Exception 6 | { 7 | public CorruptPublicKeyException() 8 | { 9 | } 10 | 11 | public CorruptPublicKeyException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public CorruptPublicKeyException(string message, Exception inner) 17 | : base(message, inner) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Minisign.Net/Exceptions/CorruptSignatureException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minisign.Exceptions 4 | { 5 | public class CorruptSignatureException : Exception 6 | { 7 | public CorruptSignatureException() 8 | { 9 | } 10 | 11 | public CorruptSignatureException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public CorruptSignatureException(string message, Exception inner) 17 | : base(message, inner) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Minisign.Net/Exceptions/FileSizeExceededException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minisign.Exceptions 4 | { 5 | public class FileSizeExceededException : Exception 6 | { 7 | public FileSizeExceededException() 8 | { 9 | } 10 | 11 | public FileSizeExceededException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public FileSizeExceededException(string message, Exception inner) 17 | : base(message, inner) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Minisign.Net/Helper/ArrayHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Minisign.Helper 5 | { 6 | /// 7 | /// Helper class for working with arrays. 8 | /// 9 | /// code courtesy of @CodesInChaos, public domain 10 | /// 11 | public static class ArrayHelpers 12 | { 13 | /// 14 | /// Concatenate the given byte arrays. 15 | /// 16 | /// 17 | /// The byte arrays. 18 | /// The concatenated byte arrays. 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static T[] ConcatArrays(params T[][] arrays) 24 | { 25 | checked 26 | { 27 | var result = new T[arrays.Sum(arr => arr.Length)]; 28 | var offset = 0; 29 | 30 | foreach (var arr in arrays) 31 | { 32 | Buffer.BlockCopy(arr, 0, result, offset, arr.Length); 33 | offset += arr.Length; 34 | } 35 | 36 | return result; 37 | } 38 | } 39 | 40 | /// 41 | /// Concatenate two byte arrays. 42 | /// 43 | /// 44 | /// The first byte array. 45 | /// The second byte array. 46 | /// The concatenated byte arrays. 47 | /// 48 | /// 49 | /// 50 | /// 51 | public static T[] ConcatArrays(T[] arr1, T[] arr2) 52 | { 53 | checked 54 | { 55 | var result = new T[arr1.Length + arr2.Length]; 56 | Buffer.BlockCopy(arr1, 0, result, 0, arr1.Length); 57 | Buffer.BlockCopy(arr2, 0, result, arr1.Length, arr2.Length); 58 | 59 | return result; 60 | } 61 | } 62 | 63 | /// 64 | /// Extract a part of a byte array from another byte array. 65 | /// 66 | /// 67 | /// A byte array. 68 | /// Position to start extraction. 69 | /// The length of the extraction started at start. 70 | /// A part with the given length of the byte array. 71 | /// 72 | /// 73 | /// 74 | public static T[] SubArray(T[] arr, int start, int length) 75 | { 76 | var result = new T[length]; 77 | Buffer.BlockCopy(arr, start, result, 0, length); 78 | 79 | return result; 80 | } 81 | 82 | /// 83 | /// Extract a part of a byte array from another byte array. 84 | /// 85 | /// 86 | /// A byte array. 87 | /// Position to start extraction. 88 | /// A part of the given byte array. 89 | /// 90 | /// 91 | /// 92 | /// 93 | public static T[] SubArray(T[] arr, int start) 94 | { 95 | return SubArray(arr, start, arr.Length - start); 96 | } 97 | 98 | /// 99 | /// Constant-time comparison of two byte arrays. 100 | /// 101 | /// The first byte array. 102 | /// The second byte array. 103 | /// true if valid; otherwise, false. 104 | /// 105 | public static bool ConstantTimeEquals(byte[] a, byte[] b) 106 | { 107 | var diff = (uint)a.Length ^ (uint)b.Length; 108 | for (var i = 0; i < a.Length && i < b.Length; i++) 109 | diff |= (uint)(a[i] ^ b[i]); 110 | return diff == 0; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Minisign.Net/Helper/EncryptionHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Minisign.Helper 4 | { 5 | /// 6 | /// Helper class for simple encryption. 7 | /// 8 | public static class EncryptionHelpers 9 | { 10 | /// 11 | /// Encrypt an array with XOR. 12 | /// 13 | /// An unencrypted array. 14 | /// The encryption keys. 15 | /// An encrypted array. 16 | public static byte[] Xor(byte[] data, IReadOnlyList keys) 17 | { 18 | for (var i = 0; i < data.Length; i++) 19 | { 20 | data[i] = (byte)(data[i] ^ keys[i]); 21 | } 22 | return data; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Minisign.Net/Minisign.Net.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Minisign 6 | true 7 | MIT 8 | bitbeans 9 | bitbeans 10 | Copyright © 2017 - 2020 bitbeans 11 | Minisign.Net is a .NET port of minisign 12 | 0.2.0.0 13 | 0.2.0.0 14 | 0.2.0 15 | https://github.com/bitbeans/minisign-net 16 | https://github.com/bitbeans/minisign-net 17 | .NET Standard 2.0 18 | minisign-net 19 | minisign libsodium 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Minisign.Net/Models/MinisignKeyPair.cs: -------------------------------------------------------------------------------- 1 | namespace Minisign.Models 2 | { 3 | public class MinisignKeyPair 4 | { 5 | public MinisignPublicKey MinisignPublicKey { get; set; } 6 | public MinisignPrivateKey MinisignPrivateKey { get; set; } 7 | public string MinisignPublicKeyFilePath { get; set; } 8 | public string MinisignPrivateKeyFilePath { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Minisign.Net/Models/MinisignPrivateKey.cs: -------------------------------------------------------------------------------- 1 | namespace Minisign.Models 2 | { 3 | public class MinisignPrivateKey 4 | { 5 | public byte[] SignatureAlgorithm { get; set; } 6 | public byte[] KdfAlgorithm { get; set; } 7 | public byte[] ChecksumAlgorithm { get; set; } 8 | public byte[] KdfSalt { get; set; } 9 | public long KdfOpsLimit { get; set; } 10 | public long KdfMemLimit { get; set; } 11 | public byte[] KeyId { get; set; } 12 | public byte[] SecretKey { get; set; } 13 | public byte[] PublicKey { get; set; } 14 | public byte[] Checksum { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Minisign.Net/Models/MinisignPublicKey.cs: -------------------------------------------------------------------------------- 1 | namespace Minisign.Models 2 | { 3 | public class MinisignPublicKey 4 | { 5 | public byte[] SignatureAlgorithm { get; set; } 6 | public byte[] KeyId { get; set; } 7 | public byte[] PublicKey { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Minisign.Net/Models/MinisignSignature.cs: -------------------------------------------------------------------------------- 1 | namespace Minisign.Models 2 | { 3 | public class MinisignSignature 4 | { 5 | public byte[] SignatureAlgorithm { get; set; } 6 | public byte[] KeyId { get; set; } 7 | public byte[] Signature { get; set; } 8 | public byte[] GlobalSignature { get; set; } 9 | public byte[] TrustedComment { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minisign.Net 2 | 3 | ![.NET Core](https://github.com/bitbeans/minisign-net/workflows/.NET%20Core/badge.svg) 4 | 5 | Minisign.Net is a .NET port of [minisign](https://github.com/jedisct1/minisign) written by @jedisct1 Frank Denis. If you are looking for a command line tool, please use the [original minisign software](https://jedisct1.github.io/minisign/). There are pre-compiled versions for any os. 6 | 7 | [minisign](https://github.com/jedisct1/minisign/blob/master/LICENSE) Copyright (c) 2015 - 2017 Frank Denis 8 | 9 | ## Available Methods 10 | 11 | ### Sign a file 12 | ```csharp 13 | public static string Sign(string fileToSign, MinisignPrivateKey minisignPrivateKey, string untrustedComment = "", string trustedComment = "", string outputFolder = "") 14 | ``` 15 | 16 | ### Validate a file 17 | ```csharp 18 | public static bool ValidateSignature(string filePath, MinisignSignature signature, MinisignPublicKey publicKey) 19 | 20 | public static bool ValidateSignature(byte[] message, MinisignSignature signature, MinisignPublicKey publicKey) 21 | ``` 22 | 23 | ### Generate a key pair 24 | ```csharp 25 | public static MinisignKeyPair GenerateKeyPair(string password, bool writeOutputFiles = false, string outputFolder = "", string keyPairFileName = "minisign") 26 | ``` 27 | 28 | ### Load a signature 29 | ```csharp 30 | public static MinisignSignature LoadSignatureFromString(string signatureString, string trustedComment, string globalSignature) 31 | 32 | public static MinisignSignature LoadSignatureFromFile(string signatureFile) 33 | 34 | public static MinisignSignature LoadSignature(byte[] signature, byte[] trustedComment, byte[] globalSignature) 35 | ``` 36 | 37 | ### Load a public key 38 | ```csharp 39 | public static MinisignPublicKey LoadPublicKeyFromString(string publicKeyString) 40 | 41 | public static MinisignPublicKey LoadPublicKeyFromFile(string publicKeyFile) 42 | 43 | public static MinisignPublicKey LoadPublicKey(byte[] publicKey) 44 | ``` 45 | 46 | ### Load a private key 47 | ```csharp 48 | public static MinisignPrivateKey LoadPrivateKeyFromString(string privateKeyString, string password) 49 | 50 | public static MinisignPrivateKey LoadPrivateKeyFromFile(string privateKeyFile, string password) 51 | 52 | public static MinisignPrivateKey LoadPrivateKey(byte[] privateKey, byte[] password) 53 | ``` 54 | 55 | 56 | ## License 57 | [MIT](https://en.wikipedia.org/wiki/MIT_License) --------------------------------------------------------------------------------