├── .gitignore ├── CryptoUtils.Test ├── CryptoUtils.Test.csproj └── TestRSAExtensions.cs ├── CryptoUtils.sln ├── CryptoUtils ├── CryptoUtils.csproj ├── RSAExtensions.cs └── UnsafeNativeMethods.cs ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 Google LLC 3 | # 4 | # Licensed to the Apache Software Foundation (ASF) under one 5 | # or more contributor license agreements. See the NOTICE file 6 | # distributed with this work for additional information 7 | # regarding copyright ownership. The ASF licenses this file 8 | # to you under the Apache License, Version 2.0 (the 9 | # "License"); you may not use this file except in compliance 10 | # with the License. You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, 15 | # software distributed under the License is distributed on an 16 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | # KIND, either express or implied. See the License for the 18 | # specific language governing permissions and limitations 19 | # under the License. 20 | # 21 | 22 | ## Ignore Visual Studio temporary files, build results, and 23 | ## files generated by popular Visual Studio add-ons. 24 | 25 | # User-specific files 26 | *.suo 27 | *.user 28 | *.userosscache 29 | *.sln.docstates 30 | 31 | # User-specific files (MonoDevelop/Xamarin Studio) 32 | *.userprefs 33 | 34 | # Build results 35 | [Dd]ebug/ 36 | [Dd]ebugPublic/ 37 | [Rr]elease/ 38 | [Rr]eleases/ 39 | x64/ 40 | x86/ 41 | bld/ 42 | [Bb]in/ 43 | [Oo]bj/ 44 | [Ll]og/ 45 | 46 | # Visual Studio 2015 cache/options directory 47 | .vs/ 48 | 49 | # NuGet Packages 50 | *.nupkg 51 | # The packages folder can be ignored because of Package Restore 52 | **/packages/* 53 | # except build/, which is used as an MSBuild target. 54 | !**/packages/build/ 55 | # Uncomment if necessary however generally it will be regenerated when needed 56 | #!**/packages/repositories.config 57 | # NuGet v3's project.json files produces more ignoreable files 58 | *.nuget.props 59 | *.nuget.targets 60 | 61 | # Visual Studio cache files 62 | # files ending in .cache can be ignored 63 | *.[Cc]ache 64 | # but keep track of directories ending in .cache 65 | !*.[Cc]ache/ -------------------------------------------------------------------------------- /CryptoUtils.Test/CryptoUtils.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net48 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CryptoUtils.Test/TestRSAExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Johannes Passing, https://jpassing.com/ 3 | // 4 | // Licensed to the Apache Software Foundation (ASF) under one 5 | // or more contributor license agreements. See the NOTICE file 6 | // distributed with this work for additional information 7 | // regarding copyright ownership. The ASF licenses this file 8 | // to you under the Apache License, Version 2.0 (the 9 | // "License"); you may not use this file except in compliance 10 | // with the License. You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, 15 | // software distributed under the License is distributed on an 16 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | // KIND, either express or implied. See the License for the 18 | // specific language governing permissions and limitations 19 | // under the License. 20 | // 21 | 22 | using NUnit.Framework; 23 | using System.Linq; 24 | using System.Security.Cryptography; 25 | 26 | namespace CryptoUtils.Test 27 | { 28 | 29 | public abstract class TestRSAExtensions 30 | { 31 | private const string RsaPublicKeyPem = 32 | @"-----BEGIN RSA PUBLIC KEY----- 33 | MIIBigKCAYEAq3DnhgYgLVJknvDA3clATozPtjI7yauqD4/ZuqgZn4KzzzkQ4BzJ 34 | ar4jRygpzbghlFn0Luk1mdVKzPUgYj0VkbRlHyYfcahbgOHixOOnXkKXrtZW7yWG 35 | jXPqy/ZJ/+kFBNPAzxy7fDuAzKfU3Rn50sBakg95pua14W1oE4rtd4/U+sg2maCq 36 | 6HgGdCLLxRWwXA8IBtvHZ48i6kxiz9tucFdS/ULvWsXjQnyE5rgs3tPhptyl2/js 37 | /6FGgdKDaPal8/tud/rPxYSuzBPp7YwRKRRN1EpYQdd4tZzeXdvOvrSIfH+ZL7Rc 38 | i+HGasbRjCom3HJL+wDGVggUkeuOUzZDjKGqZNCvZIqe5FuU0NAd8c2w2Mxaxia9 39 | 1G8jZDu92DqCEI/HoxXsZPSjd0L4EMx5HqXpYpFY2YPL95zabmynO3RCTWFN7uq6 40 | DJGlzRCTHeRDa4CvNwHCzv0kqR4uo6VlWp2dW2M/v0k1+kP70EwGqq9dnK5RMXC3 41 | XwJbrAbpGUDlAgMBAAE= 42 | -----END RSA PUBLIC KEY-----"; 43 | 44 | 45 | private const string SubjectPublicKeyInfoPem = 46 | @"-----BEGIN PUBLIC KEY----- 47 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyItKCYN/yAzDEv2HaDaq 48 | kK3J5AjerXmP1ZhBa8r5M5xQTkHPnkOkOc1KPly/xH4hmBVf00dfGZ91hTez1iD0 49 | XKkmfwP4TGXZ1YeqvlS44bvt3yZCR09aA0cGwS5Dp6xFIlz3aahMaV3gXwqaNLxW 50 | Xy5qJSZLIXhxAd0uqlnudweoMgxMbmq8vSMGmx8U8r3x2ldYhdcDYD+wAJCDGPeI 51 | vNTcHmFujYH8cMobFjewQcGDtf2lOtHn6Q15h6cuENpI5q6Rl7Xmim+Xq6fwiAf7 52 | ivRRgtOTncBgBVPhjB6vmtSP1CbF6Mpww/ZPTuavBr3dCKmywBRiVHbndOZWREnB 53 | gdY3koteVKcIVWwzLwzjPJOX1jTWGdCkX/vs6qFOgfnFOd0mDEywF+AwBAXXADw4 54 | GxZllq/lzBNf6JWNLsHLQY19ke8doCkc4/C2Gn7+xJKqM/YVWEZxVR+WhqkDCpJV 55 | wtUlPtOf2x3nNM/kM8p8pZKDU6SWNlbuRgYH2GJa8ZPrAgMBAAE= 56 | -----END PUBLIC KEY-----"; 57 | 58 | private const string EccPublicKeyPem = 59 | @"-----BEGIN PUBLIC KEY----- 60 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAACGNU1rVGpVfFyfPlx4Ydz0pQ0N 61 | 2BCrIQpSccUmJbg6v1WYfYZNR9RAQuaONRAla0dhLC6NZ7oslIEW8iNdjA== 62 | -----END PUBLIC KEY-----"; 63 | 64 | protected abstract RSA CreateKey(); 65 | 66 | private static void AssertKeysEqual(RSA expected, RSA actual) 67 | { 68 | Assert.IsTrue(Enumerable.SequenceEqual( 69 | expected.ExportParameters(false).Modulus, 70 | actual.ExportParameters(false).Modulus)); 71 | Assert.IsTrue(Enumerable.SequenceEqual( 72 | expected.ExportParameters(false).Exponent, 73 | actual.ExportParameters(false).Exponent)); 74 | } 75 | 76 | private static void AssertPemEqual(string expected, string actual) 77 | { 78 | var expectedLines = expected.Split('\n'); 79 | var actualLines = expected.Split('\n'); 80 | 81 | Assert.AreEqual(expectedLines.First(), actualLines.First()); 82 | Assert.AreEqual(expectedLines.Last(), actualLines.Last()); 83 | 84 | var expectedBody = string.Concat(expectedLines 85 | .Where(line => !line.StartsWith("-----")) 86 | .Select(line => line.Trim())); 87 | var actualBody = string.Concat(actualLines 88 | .Where(line => !line.StartsWith("-----")) 89 | .Select(line => line.Trim())); 90 | 91 | Assert.AreEqual(expectedBody, actualBody); 92 | } 93 | 94 | [Test] 95 | public void WhenKeyValid_ThenExportSubjectPublicKeyInfoReturnsDerBlob() 96 | { 97 | var originalKey = CreateKey(); 98 | var subjectPublicKeyInfoDer = originalKey.ExportSubjectPublicKeyInfo(); 99 | 100 | var reimportedKey = CreateKey(); 101 | reimportedKey.ImportSubjectPublicKeyInfo(subjectPublicKeyInfoDer, out var _); 102 | 103 | AssertKeysEqual(originalKey, reimportedKey); 104 | } 105 | 106 | [Test] 107 | public void WhenKeyValid_ThenExportRSAPublicKeyReturnsDerBlob() 108 | { 109 | var originalKey = CreateKey(); 110 | var rsaPublicKeyDer = originalKey.ExportRSAPublicKey(); 111 | 112 | var reimportedKey = CreateKey(); 113 | reimportedKey.ImportRSAPublicKey(rsaPublicKeyDer, out var _); 114 | 115 | AssertKeysEqual(originalKey, reimportedKey); 116 | } 117 | 118 | [Test] 119 | public void WhenDerIsRSAPublicKey_ThenImportSubjectPublicKeyInfoThrowsException() 120 | { 121 | var originalKey = CreateKey(); 122 | var rsaPublicKeyDer = originalKey.ExportRSAPublicKey(); 123 | 124 | var reimportedKey = CreateKey(); 125 | Assert.Throws( 126 | () => reimportedKey.ImportSubjectPublicKeyInfo(rsaPublicKeyDer, out var _)); 127 | } 128 | 129 | [Test] 130 | public void WhenDerIsSubjectPublicKeyInfo_ThenImportSubjectPublicKeyInfoThrowsException() 131 | { 132 | var originalKey = CreateKey(); 133 | var subjectPublicKeyInfoDer = originalKey.ExportSubjectPublicKeyInfo(); 134 | 135 | var reimportedKey = CreateKey(); 136 | Assert.Throws( 137 | () => reimportedKey.ImportRSAPublicKey(subjectPublicKeyInfoDer, out var _)); 138 | } 139 | 140 | [Test] 141 | public void WhenPemContainsRSAPublicKey_ThenImportFromPemSucceeds() 142 | { 143 | var importedKey = CreateKey(); 144 | importedKey.ImportFromPem(RsaPublicKeyPem, out var format); 145 | 146 | Assert.AreEqual(RsaPublicKeyFormat.RsaPublicKey, format); 147 | var exported = importedKey.ExportToPem(format); 148 | 149 | AssertPemEqual(RsaPublicKeyPem, exported); 150 | } 151 | 152 | [Test] 153 | public void WhenPemContainsSubjectPublicKeyInfo_ThenImportFromPemSucceeds() 154 | { 155 | var importedKey = CreateKey(); 156 | importedKey.ImportFromPem(SubjectPublicKeyInfoPem, out var format); 157 | 158 | Assert.AreEqual(RsaPublicKeyFormat.SubjectPublicKeyInfo, format); 159 | var exported = importedKey.ExportToPem(format); 160 | 161 | AssertPemEqual(SubjectPublicKeyInfoPem, exported); 162 | } 163 | 164 | [Test] 165 | public void WhenPemContainsEccPublicKey_ThenImportFromPemThrowsException() 166 | { 167 | var importedKey = CreateKey(); 168 | Assert.Throws( 169 | () => importedKey.ImportFromPem(EccPublicKeyPem, out var format)); 170 | } 171 | } 172 | 173 | [TestFixture] 174 | public class TestRSAExtensions_CNG : TestRSAExtensions 175 | { 176 | protected override RSA CreateKey() => new RSACng(); 177 | } 178 | 179 | [TestFixture] 180 | public class TestRSAExtensions_CryptoServiceProvicer : TestRSAExtensions 181 | { 182 | protected override RSA CreateKey() => new RSACryptoServiceProvider(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /CryptoUtils.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31410.357 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoUtils", "CryptoUtils\CryptoUtils.csproj", "{B0F52432-ED02-4750-BFFB-5E1221A625E9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoUtils.Test", "CryptoUtils.Test\CryptoUtils.Test.csproj", "{24C8A061-679A-44A0-9015-CCB26FF9F20D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B0F52432-ED02-4750-BFFB-5E1221A625E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B0F52432-ED02-4750-BFFB-5E1221A625E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B0F52432-ED02-4750-BFFB-5E1221A625E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B0F52432-ED02-4750-BFFB-5E1221A625E9}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {24C8A061-679A-44A0-9015-CCB26FF9F20D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {24C8A061-679A-44A0-9015-CCB26FF9F20D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {24C8A061-679A-44A0-9015-CCB26FF9F20D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {24C8A061-679A-44A0-9015-CCB26FF9F20D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {521733FC-41D0-4280-89E2-60A41818BA0D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CryptoUtils/CryptoUtils.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1;net48 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CryptoUtils/RSAExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Johannes Passing, https://jpassing.com/ 3 | // 4 | // Licensed to the Apache Software Foundation (ASF) under one 5 | // or more contributor license agreements. See the NOTICE file 6 | // distributed with this work for additional information 7 | // regarding copyright ownership. The ASF licenses this file 8 | // to you under the Apache License, Version 2.0 (the 9 | // "License"); you may not use this file except in compliance 10 | // with the License. You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, 15 | // software distributed under the License is distributed on an 16 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | // KIND, either express or implied. See the License for the 18 | // specific language governing permissions and limitations 19 | // under the License. 20 | // 21 | 22 | using System; 23 | using System.ComponentModel; 24 | using System.Linq; 25 | using System.Runtime.InteropServices; 26 | using System.Security.Cryptography; 27 | using System.Text; 28 | 29 | namespace CryptoUtils 30 | { 31 | public static class RSAExtensions 32 | { 33 | private const string RsaPublickeyPemHeader = "-----BEGIN RSA PUBLIC KEY-----"; 34 | private const string RsaPublickeyPemFooter = "-----END RSA PUBLIC KEY-----"; 35 | private const string SubjectPublicKeyInfoPemHeader = "-----BEGIN PUBLIC KEY-----"; 36 | private const string SubjectPublicKeyInfoPemFooter = "-----END PUBLIC KEY-----"; 37 | 38 | private const string RsaOid = "1.2.840.113549.1.1.1"; 39 | 40 | //--------------------------------------------------------------------- 41 | // NetFx surrogate implementations for methods available in .NET Core/ 42 | // .NET 5+. 43 | //--------------------------------------------------------------------- 44 | 45 | #if NET40_OR_GREATER 46 | 47 | private static void ImportCspBlob( 48 | RSA key, 49 | byte[] cspBlob) 50 | { 51 | if (key is RSACng) 52 | { 53 | // 54 | // RSACng.Key is private, so we can't import into 55 | // an existing key directly. But we can do so 56 | // indirectly. 57 | // 58 | var importedKey = CngKey.Import(cspBlob, CngKeyBlobFormat.GenericPublicBlob); 59 | var importedKeyParameters = new RSACng(importedKey).ExportParameters(false); 60 | key.ImportParameters(importedKeyParameters); 61 | } 62 | else if (key is RSACryptoServiceProvider cryptoApiKey) 63 | { 64 | cryptoApiKey.ImportCspBlob(cspBlob); 65 | } 66 | else 67 | { 68 | throw new ArgumentException("Unrecognized key type"); 69 | } 70 | } 71 | 72 | private static byte[] ExportCspBlob( 73 | RSA key, 74 | out uint cspBlobType) 75 | { 76 | // 77 | // CNG and CryptoAPI use different key blob formats, and expose 78 | // different APIs to create them. 79 | // 80 | if (key is RSACng cngKey) 81 | { 82 | cspBlobType = UnsafeNativeMethods.CNG_RSA_PUBLIC_KEY_BLOB; 83 | return cngKey.Key.Export(CngKeyBlobFormat.GenericPublicBlob); 84 | } 85 | else if (key is RSACryptoServiceProvider cryptoApiKey) 86 | { 87 | cspBlobType = UnsafeNativeMethods.RSA_CSP_PUBLICKEYBLOB; 88 | return cryptoApiKey.ExportCspBlob(false); 89 | } 90 | else 91 | { 92 | throw new ArgumentException("Unrecognized key type"); 93 | } 94 | } 95 | 96 | /// 97 | /// Exports the public-key portion of the current key in the X.509 98 | /// SubjectPublicKeyInfo 99 | /// format. 100 | /// 101 | /// 102 | /// A byte array containing the X.509 SubjectPublicKeyInfo representation of the 103 | /// public-key portion of this key. 104 | /// 105 | public static byte[] ExportSubjectPublicKeyInfo(this RSA key) 106 | { 107 | byte[] cspBlob = ExportCspBlob(key, out uint cspBlobType); 108 | 109 | // 110 | // Decode CSP blob -> RSA PublicKey DER. 111 | // 112 | using (var cspBlobHandle = LocalAllocHandle.Alloc(cspBlob.Length)) 113 | { 114 | Marshal.Copy( 115 | cspBlob, 116 | 0, 117 | cspBlobHandle.DangerousGetHandle(), 118 | cspBlob.Length); 119 | 120 | if (UnsafeNativeMethods.CryptEncodeObjectEx( 121 | UnsafeNativeMethods.X509_ASN_ENCODING | 122 | UnsafeNativeMethods.PKCS_7_ASN_ENCODING, 123 | cspBlobType, 124 | cspBlobHandle.DangerousGetHandle(), 125 | UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG, 126 | IntPtr.Zero, 127 | out var rsaDerHandle, 128 | out uint rsaDerSize)) 129 | { 130 | using (rsaDerHandle) 131 | { 132 | // 133 | // Wrap the RSA PublicKey DER blob into a CERT_PUBLIC_KEY_INFO. 134 | // 135 | var certKeyInfo = new UnsafeNativeMethods.CERT_PUBLIC_KEY_INFO() 136 | { 137 | Algorithm = new UnsafeNativeMethods.CRYPT_ALGORITHM_IDENTIFIER() 138 | { 139 | pszObjId = RsaOid 140 | }, 141 | PublicKey = new UnsafeNativeMethods.CRYPT_BIT_BLOB() 142 | { 143 | pbData = rsaDerHandle.DangerousGetHandle(), 144 | cbData = rsaDerSize 145 | } 146 | }; 147 | 148 | // 149 | // Encode CERT_PUBLIC_KEY_INFO -> DER. 150 | // 151 | using (var certKeyInfoHandle = LocalAllocHandle.Alloc( 152 | Marshal.SizeOf())) 153 | { 154 | Marshal.StructureToPtr( 155 | certKeyInfo, 156 | certKeyInfoHandle.DangerousGetHandle(), 157 | false); 158 | 159 | if (UnsafeNativeMethods.CryptEncodeObjectEx( 160 | UnsafeNativeMethods.X509_ASN_ENCODING | 161 | UnsafeNativeMethods.PKCS_7_ASN_ENCODING, 162 | UnsafeNativeMethods.X509_PUBLIC_KEY_INFO, 163 | certKeyInfoHandle.DangerousGetHandle(), 164 | UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG, 165 | IntPtr.Zero, 166 | out var certKeyInfoDerHandle, 167 | out uint certKeyInfoDerSize)) 168 | { 169 | using (certKeyInfoDerHandle) 170 | { 171 | var certKeyInfoDer = new byte[certKeyInfoDerSize]; 172 | Marshal.Copy( 173 | certKeyInfoDerHandle.DangerousGetHandle(), 174 | certKeyInfoDer, 175 | 0, 176 | (int)certKeyInfoDerSize); 177 | return certKeyInfoDer; 178 | } 179 | } 180 | else 181 | { 182 | throw new CryptographicException( 183 | "Failed to encode CERT_PUBLIC_KEY_INFO", 184 | new Win32Exception()); 185 | } 186 | } 187 | } 188 | } 189 | else 190 | { 191 | throw new CryptographicException( 192 | "Failed to encode CSP blob", 193 | new Win32Exception()); 194 | } 195 | } 196 | } 197 | 198 | /// 199 | /// Exports the public-key portion of the current key in the PKCS#1 RSAPublicKey 200 | /// format. 201 | /// 202 | /// 203 | /// A byte array containing the PKCS#1 RSAPublicKey representation of this key. 204 | /// 205 | public static byte[] ExportRSAPublicKey(this RSA key) 206 | { 207 | byte[] cspBlob = ExportCspBlob(key, out uint cspBlobType); 208 | 209 | // 210 | // Decode CSP blob -> RSA PublicKey DER. 211 | // 212 | using (var cspBlobHandle = LocalAllocHandle.Alloc(cspBlob.Length)) 213 | { 214 | Marshal.Copy( 215 | cspBlob, 216 | 0, 217 | cspBlobHandle.DangerousGetHandle(), 218 | cspBlob.Length); 219 | 220 | if (UnsafeNativeMethods.CryptEncodeObjectEx( 221 | UnsafeNativeMethods.X509_ASN_ENCODING | 222 | UnsafeNativeMethods.PKCS_7_ASN_ENCODING, 223 | cspBlobType, 224 | cspBlobHandle.DangerousGetHandle(), 225 | UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG, 226 | IntPtr.Zero, 227 | out var derBlobHandle, 228 | out uint derBlobSize)) 229 | { 230 | using (derBlobHandle) 231 | { 232 | var derBlob = new byte[derBlobSize]; 233 | Marshal.Copy( 234 | derBlobHandle.DangerousGetHandle(), 235 | derBlob, 236 | 0, 237 | (int)derBlobSize); 238 | return derBlob; 239 | } 240 | } 241 | else 242 | { 243 | throw new CryptographicException( 244 | "Failed to encode CSP blob", 245 | new Win32Exception()); 246 | } 247 | } 248 | } 249 | 250 | /// 251 | /// Imports the public key from a PKCS#1 RSAPublicKey structure after decryption, 252 | /// replacing the keys for this object. 253 | /// 254 | /// 255 | /// The bytes of a PKCS#1 RSAPublicKey structure in the ASN.1-BER encoding. 256 | /// 257 | /// 258 | /// When this method returns, contains a value that indicates the number of bytes 259 | /// read from source. This parameter is treated as uninitialized. 260 | /// 261 | public static void ImportRSAPublicKey( 262 | this RSA key, 263 | byte[] derBlob, 264 | out int bytesRead) 265 | { 266 | using (var derBlobHandle = LocalAllocHandle.Alloc(derBlob.Length)) 267 | { 268 | Marshal.Copy( 269 | derBlob, 270 | 0, 271 | derBlobHandle.DangerousGetHandle(), 272 | derBlob.Length); 273 | 274 | // 275 | // Decode RSA PublicKey DER -> CSP blob. 276 | // 277 | if (UnsafeNativeMethods.CryptDecodeObjectEx( 278 | UnsafeNativeMethods.X509_ASN_ENCODING | 279 | UnsafeNativeMethods.PKCS_7_ASN_ENCODING, 280 | UnsafeNativeMethods.RSA_CSP_PUBLICKEYBLOB, 281 | derBlobHandle.DangerousGetHandle(), 282 | (uint)derBlob.Length, 283 | UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG, 284 | IntPtr.Zero, 285 | out var keyBlobHandle, 286 | out var keyBlobSize)) 287 | { 288 | using (keyBlobHandle) 289 | { 290 | var keyBlobBytes = new byte[keyBlobSize]; 291 | Marshal.Copy( 292 | keyBlobHandle.DangerousGetHandle(), 293 | keyBlobBytes, 294 | 0, 295 | (int)keyBlobSize); 296 | 297 | bytesRead = derBlob.Length; 298 | ImportCspBlob(key, keyBlobBytes); 299 | } 300 | } 301 | else 302 | { 303 | throw new CryptographicException( 304 | "Failed to decode DER blob", 305 | new Win32Exception()); 306 | } 307 | } 308 | } 309 | 310 | /// 311 | /// Imports the public key from an X.509 SubjectPublicKeyInfo structure 312 | /// after decryption, 313 | /// replacing the keys for this object. 314 | /// 315 | /// 316 | /// The bytes of an X.509 SubjectPublicKeyInfo structure in the ASN.1-DER encoding. 317 | /// 318 | /// 319 | /// When this method returns, contains a value that indicates the number of bytes 320 | /// read from source. This parameter is treated as uninitialized. 321 | /// 322 | public static void ImportSubjectPublicKeyInfo( 323 | this RSA key, 324 | byte[] certKeyInfoDer, 325 | out int bytesRead) 326 | 327 | { 328 | using (var certKeyInfoDerHandle = LocalAllocHandle.Alloc(certKeyInfoDer.Length)) 329 | { 330 | Marshal.Copy( 331 | certKeyInfoDer, 332 | 0, 333 | certKeyInfoDerHandle.DangerousGetHandle(), 334 | certKeyInfoDer.Length); 335 | 336 | // 337 | // Decode DER -> CERT_PUBLIC_KEY_INFO. 338 | // 339 | if (UnsafeNativeMethods.CryptDecodeObjectEx( 340 | UnsafeNativeMethods.X509_ASN_ENCODING | 341 | UnsafeNativeMethods.PKCS_7_ASN_ENCODING, 342 | UnsafeNativeMethods.X509_PUBLIC_KEY_INFO, 343 | certKeyInfoDerHandle.DangerousGetHandle(), 344 | (uint)certKeyInfoDer.Length, 345 | UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG, 346 | IntPtr.Zero, 347 | out var certKeyInfoHandle, 348 | out var certKeyInfoSize)) 349 | { 350 | using (certKeyInfoHandle) 351 | { 352 | // 353 | // Check that the CERT_PUBLIC_KEY_INFO contains an RSA public key. 354 | // 355 | var certInfo = Marshal.PtrToStructure( 356 | certKeyInfoHandle.DangerousGetHandle()); 357 | 358 | if (certInfo.Algorithm.pszObjId != RsaOid) 359 | { 360 | throw new CryptographicException("Not an RSA public key"); 361 | } 362 | 363 | // 364 | // Decode the RSA public key -> CSP blob. 365 | // 366 | if (UnsafeNativeMethods.CryptDecodeObjectEx( 367 | UnsafeNativeMethods.X509_ASN_ENCODING | 368 | UnsafeNativeMethods.PKCS_7_ASN_ENCODING, 369 | UnsafeNativeMethods.RSA_CSP_PUBLICKEYBLOB, 370 | certInfo.PublicKey.pbData, 371 | certInfo.PublicKey.cbData, 372 | UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG, 373 | IntPtr.Zero, 374 | out var cspKeyBlob, 375 | out var cspKeyBlobSize)) 376 | { 377 | using (cspKeyBlob) 378 | { 379 | var keyBlobBytes = new byte[cspKeyBlobSize]; 380 | Marshal.Copy( 381 | cspKeyBlob.DangerousGetHandle(), 382 | keyBlobBytes, 383 | 0, 384 | (int)cspKeyBlobSize); 385 | 386 | bytesRead = certKeyInfoDer.Length; 387 | ImportCspBlob(key, keyBlobBytes); 388 | } 389 | } 390 | else 391 | { 392 | throw new CryptographicException( 393 | "Failed to decode RSA public key from CERT_PUBLIC_KEY_INFO", 394 | new Win32Exception()); 395 | } 396 | } 397 | } 398 | else 399 | { 400 | throw new CryptographicException( 401 | "Failed to decode DER blob into CERT_PUBLIC_KEY_INFO", 402 | new Win32Exception()); 403 | } 404 | } 405 | } 406 | #endif 407 | 408 | //--------------------------------------------------------------------- 409 | // Convenience methods for reading/writing PEM-encoded keys. 410 | //--------------------------------------------------------------------- 411 | 412 | #if !(NET5_0 || NET5_0_OR_GREATER) 413 | public static void ImportFromPem( 414 | this RSA key, 415 | string source) 416 | => ImportFromPem(key, source, out var _); 417 | #endif 418 | 419 | public static void ImportFromPem( 420 | this RSA key, 421 | string source, 422 | out RsaPublicKeyFormat format) 423 | { 424 | source = source.Trim(); 425 | 426 | // 427 | // Inspect header to determine format. 428 | // 429 | if (source.StartsWith(SubjectPublicKeyInfoPemHeader) && 430 | source.EndsWith(SubjectPublicKeyInfoPemFooter)) 431 | { 432 | format = RsaPublicKeyFormat.SubjectPublicKeyInfo; 433 | } 434 | else if (source.StartsWith(RsaPublickeyPemHeader) && 435 | source.EndsWith(RsaPublickeyPemFooter)) 436 | { 437 | format = RsaPublicKeyFormat.RsaPublicKey; 438 | } 439 | else 440 | { 441 | throw new FormatException("Missing Public key header/footer"); 442 | } 443 | 444 | // 445 | // Decode body to get DER blob. 446 | // 447 | var der = Convert.FromBase64String(string.Concat( 448 | source 449 | .Split('\n') 450 | .Select(s => s.Trim()) 451 | .Where(line => !line.StartsWith("-----")))); 452 | if (format == RsaPublicKeyFormat.RsaPublicKey) 453 | { 454 | key.ImportRSAPublicKey(der, out var _); 455 | } 456 | else 457 | { 458 | key.ImportSubjectPublicKeyInfo(der, out var _); 459 | } 460 | } 461 | 462 | public static string ExportToPem( 463 | this RSA key, 464 | RsaPublicKeyFormat format) 465 | { 466 | var buffer = new StringBuilder(); 467 | 468 | if (format == RsaPublicKeyFormat.RsaPublicKey) 469 | { 470 | buffer.AppendLine(RsaPublickeyPemHeader); 471 | buffer.AppendLine(Convert.ToBase64String( 472 | key.ExportRSAPublicKey(), 473 | Base64FormattingOptions.InsertLineBreaks)); 474 | buffer.AppendLine(RsaPublickeyPemFooter); 475 | } 476 | else if (format == RsaPublicKeyFormat.SubjectPublicKeyInfo) 477 | { 478 | buffer.AppendLine(SubjectPublicKeyInfoPemHeader); 479 | buffer.AppendLine(Convert.ToBase64String( 480 | key.ExportSubjectPublicKeyInfo(), 481 | Base64FormattingOptions.InsertLineBreaks)); 482 | buffer.AppendLine(SubjectPublicKeyInfoPemFooter); 483 | } 484 | else 485 | { 486 | throw new ArgumentException(nameof(format)); 487 | } 488 | 489 | return buffer.ToString(); 490 | } 491 | } 492 | 493 | public enum RsaPublicKeyFormat 494 | { 495 | /// 496 | /// -----BEGIN RSA PUBLIC KEY----- 497 | /// 498 | RsaPublicKey, 499 | 500 | /// 501 | /// -----BEGIN PUBLIC KEY----- 502 | /// 503 | SubjectPublicKeyInfo 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /CryptoUtils/UnsafeNativeMethods.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2021 Johannes Passing, https://jpassing.com/ 3 | // 4 | // Licensed to the Apache Software Foundation (ASF) under one 5 | // or more contributor license agreements. See the NOTICE file 6 | // distributed with this work for additional information 7 | // regarding copyright ownership. The ASF licenses this file 8 | // to you under the Apache License, Version 2.0 (the 9 | // "License"); you may not use this file except in compliance 10 | // with the License. You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, 15 | // software distributed under the License is distributed on an 16 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | // KIND, either express or implied. See the License for the 18 | // specific language governing permissions and limitations 19 | // under the License. 20 | // 21 | 22 | using Microsoft.Win32.SafeHandles; 23 | using System; 24 | using System.Runtime.ConstrainedExecution; 25 | using System.Runtime.InteropServices; 26 | 27 | namespace CryptoUtils 28 | { 29 | internal class UnsafeNativeMethods 30 | { 31 | [DllImport("crypt32.dll", SetLastError = true)] 32 | [return: MarshalAs(UnmanagedType.Bool)] 33 | internal static extern bool CryptDecodeObjectEx( 34 | uint dwCertEncodingType, 35 | uint lpszStructType, 36 | IntPtr pbEncoded, 37 | uint cbEncoded, 38 | uint dwFlags, 39 | IntPtr pDecodePara, 40 | out LocalAllocHandle pvStructInfo, 41 | out uint pcbStructInfo); 42 | 43 | [DllImport("crypt32.dll", SetLastError = true)] 44 | [return: MarshalAs(UnmanagedType.Bool)] 45 | internal static extern bool CryptEncodeObjectEx( 46 | uint dwCertEncodingType, 47 | uint lpszStructType, 48 | IntPtr pvStructInfo, 49 | uint dwFlags, 50 | IntPtr pDecodePara, 51 | out LocalAllocHandle pvEncoded, 52 | out uint pcbEncoded); 53 | 54 | public const uint X509_ASN_ENCODING = 0x1; 55 | public const uint PKCS_7_ASN_ENCODING = 0x10000; 56 | public const uint CRYPT_DECODE_ALLOC_FLAG = 0x8000; 57 | 58 | // 59 | // Constants for CryptEncodeObject and CryptDecodeObject. 60 | // 61 | // https://docs.microsoft.com/en-us/windows/win32/seccrypto/constants-for-cryptencodeobject-and-cryptdecodeobject 62 | // 63 | public const uint X509_PUBLIC_KEY_INFO = 8; 64 | public const uint RSA_CSP_PUBLICKEYBLOB = 19; 65 | public const uint CNG_RSA_PUBLIC_KEY_BLOB = 72; 66 | 67 | [StructLayout(LayoutKind.Sequential)] 68 | public struct CRYPT_ALGORITHM_IDENTIFIER 69 | { 70 | [MarshalAs(UnmanagedType.LPStr)] public string pszObjId; 71 | public int cbData; 72 | public IntPtr pbData; 73 | } 74 | 75 | [StructLayout(LayoutKind.Sequential)] 76 | public struct CRYPT_BIT_BLOB 77 | { 78 | public uint cbData; 79 | public IntPtr pbData; 80 | public uint cUnusedBits; 81 | } 82 | 83 | [StructLayout(LayoutKind.Sequential)] 84 | public struct CERT_PUBLIC_KEY_INFO 85 | { 86 | public CRYPT_ALGORITHM_IDENTIFIER Algorithm; 87 | public CRYPT_BIT_BLOB PublicKey; 88 | } 89 | } 90 | 91 | internal sealed class LocalAllocHandle : SafeHandleZeroOrMinusOneIsInvalid 92 | { 93 | private LocalAllocHandle() : base(ownsHandle: true) { } 94 | 95 | public static LocalAllocHandle Alloc(int cb) 96 | { 97 | LocalAllocHandle handle = new LocalAllocHandle(); 98 | handle.AllocCore(cb); 99 | return handle; 100 | } 101 | 102 | [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 103 | private void AllocCore(int cb) 104 | { 105 | SetHandle(Marshal.AllocHGlobal(cb)); 106 | } 107 | 108 | protected override bool ReleaseHandle() 109 | { 110 | Marshal.FreeHGlobal(handle); 111 | return true; 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpassing/dotnet-crypto-utils/5fd689acde441fbf1c006a7c25d04bf4c7594580/README.md --------------------------------------------------------------------------------