├── .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
--------------------------------------------------------------------------------