|
2 | 2 | using System.Buffers.Text;
|
3 | 3 | using System.Globalization;
|
4 | 4 | using System.IO;
|
5 |
| -using System.Linq; |
6 | 5 | using System.Net;
|
7 | 6 | using System.Runtime.InteropServices;
|
8 | 7 | using System.Reflection;
|
@@ -68,106 +67,119 @@ public static unsafe void Convert(this Encoder encoder, ReadOnlySpan<char> chars
|
68 | 67 | #endif
|
69 | 68 |
|
70 | 69 | /// <summary>
|
71 |
| - /// Loads a RSA public key from a PEM string. Taken from <a href="https://stackoverflow.com/a/32243171/23633">Stack Overflow</a>. |
| 70 | + /// Loads a RSA public key from a PEM string. |
72 | 71 | /// </summary>
|
73 | 72 | /// <param name="publicKey">The public key, in PEM format.</param>
|
74 | 73 | /// <returns>An RSA public key, or <c>null</c> on failure.</returns>
|
75 | 74 | public static RSA DecodeX509PublicKey(string publicKey)
|
76 | 75 | {
|
77 | 76 | var x509Key = System.Convert.FromBase64String(publicKey.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", ""));
|
| 77 | + var parameters = GetKeyParameters(x509Key, false); |
| 78 | + var rsa = RSA.Create(); |
| 79 | + rsa.ImportParameters(parameters); |
| 80 | + return rsa; |
| 81 | + } |
| 82 | + |
| 83 | + // Derived from: https://stackoverflow.com/a/32243171/, https://stackoverflow.com/a/26978561/, http://luca.ntop.org/Teaching/Appunti/asn1.html |
| 84 | + internal static RSAParameters GetKeyParameters(ReadOnlySpan<byte> data, bool isPrivate) |
| 85 | + { |
| 86 | + // read header (30 81 xx, or 30 82 xx xx) |
| 87 | + if (data[0] != 0x30) |
| 88 | + throw new FormatException("Expected 0x30 but read {0:X2}".FormatInvariant(data[0])); |
| 89 | + data = data.Slice(1); |
78 | 90 |
|
79 |
| - // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" |
80 |
| - byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; |
| 91 | + if (!TryReadAsnLength(data, out var length, out var bytesConsumed)) |
| 92 | + throw new FormatException("Couldn't read key length"); |
| 93 | + data = data.Slice(bytesConsumed); |
81 | 94 |
|
82 |
| - // --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------ |
83 |
| - using (var stream = new MemoryStream(x509Key)) |
84 |
| - using (var reader = new BinaryReader(stream)) //wrap Memory Stream with BinaryReader for easy reading |
| 95 | + if (!isPrivate) |
85 | 96 | {
|
86 |
| - var temp = reader.ReadUInt16(); |
87 |
| - switch (temp) |
88 |
| - { |
89 |
| - case 0x8130: |
90 |
| - reader.ReadByte(); //advance 1 byte |
91 |
| - break; |
92 |
| - case 0x8230: |
93 |
| - reader.ReadInt16(); //advance 2 bytes |
94 |
| - break; |
95 |
| - default: |
96 |
| - throw new FormatException("Expected 0x8130 or 0x8230 but read {0:X4}".FormatInvariant(temp)); |
97 |
| - } |
| 97 | + if (!data.Slice(0, s_rsaOid.Length).SequenceEqual(s_rsaOid)) |
| 98 | + throw new FormatException("Expected RSA OID but read {0}".FormatInvariant(BitConverter.ToString(data.Slice(0, 15).ToArray()))); |
| 99 | + data = data.Slice(s_rsaOid.Length); |
| 100 | + |
| 101 | + // BIT STRING (0x03) followed by length |
| 102 | + if (data[0] != 0x03) |
| 103 | + throw new FormatException("Expected 0x03 but read {0:X2}".FormatInvariant(data[0])); |
| 104 | + data = data.Slice(1); |
| 105 | + |
| 106 | + if (!TryReadAsnLength(data, out length, out bytesConsumed)) |
| 107 | + throw new FormatException("Couldn't read length"); |
| 108 | + data = data.Slice(bytesConsumed); |
| 109 | + |
| 110 | + // skip NULL byte |
| 111 | + if (data[0] != 0x00) |
| 112 | + throw new FormatException("Expected 0x00 but read {0:X2}".FormatInvariant(data[0])); |
| 113 | + data = data.Slice(1); |
| 114 | + |
| 115 | + // skip next header (30 81 xx, or 30 82 xx xx) |
| 116 | + if (data[0] != 0x30) |
| 117 | + throw new FormatException("Expected 0x30 but read {0:X2}".FormatInvariant(data[0])); |
| 118 | + data = data.Slice(1); |
| 119 | + |
| 120 | + if (!TryReadAsnLength(data, out length, out bytesConsumed)) |
| 121 | + throw new FormatException("Couldn't read length"); |
| 122 | + data = data.Slice(bytesConsumed); |
| 123 | + } |
| 124 | + else |
| 125 | + { |
| 126 | + if (!TryReadAsnInteger(data, out var zero, out bytesConsumed) || zero.Length != 1 || zero[0] != 0) |
| 127 | + throw new FormatException("Couldn't read zero."); |
| 128 | + data = data.Slice(bytesConsumed); |
| 129 | + } |
98 | 130 |
|
99 |
| - var seq = reader.ReadBytes(15); |
100 |
| - if (!seq.SequenceEqual(seqOid)) //make sure Sequence for OID is correct |
101 |
| - throw new FormatException("Expected RSA OID but read {0}".FormatInvariant(BitConverter.ToString(seq))); |
102 |
| - |
103 |
| - temp = reader.ReadUInt16(); |
104 |
| - if (temp == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81) |
105 |
| - reader.ReadByte(); //advance 1 byte |
106 |
| - else if (temp == 0x8203) |
107 |
| - reader.ReadInt16(); //advance 2 bytes |
108 |
| - else |
109 |
| - throw new FormatException("Expected 0x8130 or 0x8230 but read {0:X4}".FormatInvariant(temp)); |
110 |
| - |
111 |
| - var bt = reader.ReadByte(); |
112 |
| - if (bt != 0x00) //expect null byte next |
113 |
| - throw new FormatException("Expected 0x00 but read {0:X2}".FormatInvariant(bt)); |
114 |
| - |
115 |
| - temp = reader.ReadUInt16(); |
116 |
| - if (temp == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) |
117 |
| - reader.ReadByte(); //advance 1 byte |
118 |
| - else if (temp == 0x8230) |
119 |
| - reader.ReadInt16(); //advance 2 bytes |
120 |
| - else |
121 |
| - throw new FormatException("Expected 0x8130 or 0x8230 but read {0:X4}".FormatInvariant(temp)); |
122 |
| - |
123 |
| - temp = reader.ReadUInt16(); |
124 |
| - byte lowbyte; |
125 |
| - byte highbyte = 0x00; |
126 |
| - |
127 |
| - if (temp == 0x8102) |
128 |
| - { |
129 |
| - //data read as little endian order (actual data order for Integer is 02 81) |
130 |
| - lowbyte = reader.ReadByte(); // read next bytes which is bytes in modulus |
131 |
| - } |
132 |
| - else if (temp == 0x8202) |
133 |
| - { |
134 |
| - highbyte = reader.ReadByte(); //advance 2 bytes |
135 |
| - lowbyte = reader.ReadByte(); |
136 |
| - } |
137 |
| - else |
| 131 | + if (!TryReadAsnInteger(data, out var modulus, out bytesConsumed)) |
| 132 | + throw new FormatException("Couldn't read modulus"); |
| 133 | + data = data.Slice(bytesConsumed); |
| 134 | + |
| 135 | + if (!TryReadAsnInteger(data, out var exponent, out bytesConsumed)) |
| 136 | + throw new FormatException("Couldn't read exponent"); |
| 137 | + data = data.Slice(bytesConsumed); |
| 138 | + |
| 139 | + if (!isPrivate) |
| 140 | + { |
| 141 | + return new RSAParameters |
138 | 142 | {
|
139 |
| - throw new FormatException("Expected 0x8102 or 0x8202 but read {0:X4}".FormatInvariant(temp)); |
140 |
| - } |
| 143 | + Modulus = modulus.ToArray(), |
| 144 | + Exponent = exponent.ToArray(), |
| 145 | + }; |
| 146 | + } |
141 | 147 |
|
142 |
| - var modulusSize = highbyte * 256 + lowbyte; |
| 148 | + if (!TryReadAsnInteger(data, out var d, out bytesConsumed)) |
| 149 | + throw new FormatException("Couldn't read D"); |
| 150 | + data = data.Slice(bytesConsumed); |
143 | 151 |
|
144 |
| - var firstbyte = reader.ReadByte(); |
145 |
| - reader.BaseStream.Seek(-1, SeekOrigin.Current); |
| 152 | + if (!TryReadAsnInteger(data, out var p, out bytesConsumed)) |
| 153 | + throw new FormatException("Couldn't read P"); |
| 154 | + data = data.Slice(bytesConsumed); |
146 | 155 |
|
147 |
| - if (firstbyte == 0x00) |
148 |
| - { |
149 |
| - //if first byte (highest order) of modulus is zero, don't include it |
150 |
| - reader.ReadByte(); //skip this null byte |
151 |
| - modulusSize -= 1; //reduce modulus buffer size by 1 |
152 |
| - } |
| 156 | + if (!TryReadAsnInteger(data, out var q, out bytesConsumed)) |
| 157 | + throw new FormatException("Couldn't read Q"); |
| 158 | + data = data.Slice(bytesConsumed); |
153 | 159 |
|
154 |
| - var modulus = reader.ReadBytes(modulusSize); //read the modulus bytes |
| 160 | + if (!TryReadAsnInteger(data, out var dp, out bytesConsumed)) |
| 161 | + throw new FormatException("Couldn't read DP"); |
| 162 | + data = data.Slice(bytesConsumed); |
155 | 163 |
|
156 |
| - if (reader.ReadByte() != 0x02) //expect an Integer for the exponent data |
157 |
| - throw new FormatException("Expected 0x02"); |
158 |
| - int exponentSize = reader.ReadByte(); // should only need one byte for actual exponent data (for all useful values) |
159 |
| - var exponent = reader.ReadBytes(exponentSize); |
| 164 | + if (!TryReadAsnInteger(data, out var dq, out bytesConsumed)) |
| 165 | + throw new FormatException("Couldn't read DQ"); |
| 166 | + data = data.Slice(bytesConsumed); |
160 | 167 |
|
161 |
| - // ------- create RSACryptoServiceProvider instance and initialize with public key ----- |
162 |
| - var rsa = RSA.Create(); |
163 |
| - var rsaKeyInfo = new RSAParameters |
164 |
| - { |
165 |
| - Modulus = modulus, |
166 |
| - Exponent = exponent |
167 |
| - }; |
168 |
| - rsa.ImportParameters(rsaKeyInfo); |
169 |
| - return rsa; |
170 |
| - } |
| 168 | + if (!TryReadAsnInteger(data, out var iq, out bytesConsumed)) |
| 169 | + throw new FormatException("Couldn't read IQ"); |
| 170 | + data = data.Slice(bytesConsumed); |
| 171 | + |
| 172 | + return new RSAParameters |
| 173 | + { |
| 174 | + Modulus = modulus.ToArray(), |
| 175 | + Exponent = exponent.ToArray(), |
| 176 | + D = d.ToArray(), |
| 177 | + P = p.ToArray(), |
| 178 | + Q = q.ToArray(), |
| 179 | + DP = dp.ToArray(), |
| 180 | + DQ = dq.ToArray(), |
| 181 | + InverseQ = iq.ToArray(), |
| 182 | + }; |
171 | 183 | }
|
172 | 184 |
|
173 | 185 | /// <summary>
|
@@ -429,5 +441,71 @@ public static SslProtocols GetDefaultSslProtocols()
|
429 | 441 | #else
|
430 | 442 | public static SslProtocols GetDefaultSslProtocols() => SslProtocols.None;
|
431 | 443 | #endif
|
| 444 | + |
| 445 | + // Reads a length encoded according to ASN.1 BER rules. |
| 446 | + private static bool TryReadAsnLength(ReadOnlySpan<byte> data, out int length, out int bytesConsumed) |
| 447 | + { |
| 448 | + var leadByte = data[0]; |
| 449 | + if (leadByte < 0x80) |
| 450 | + { |
| 451 | + // Short form. One octet. Bit 8 has value "0" and bits 7-1 give the length. |
| 452 | + length = leadByte; |
| 453 | + bytesConsumed = 1; |
| 454 | + return true; |
| 455 | + } |
| 456 | + |
| 457 | + // Long form. Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. Second and following octets give the length, base 256, most significant digit first. |
| 458 | + if (leadByte == 0x81) |
| 459 | + { |
| 460 | + length = data[1]; |
| 461 | + bytesConsumed = 2; |
| 462 | + return true; |
| 463 | + } |
| 464 | + |
| 465 | + if (leadByte == 0x82) |
| 466 | + { |
| 467 | + length = data[1] * 256 + data[2]; |
| 468 | + bytesConsumed = 3; |
| 469 | + return true; |
| 470 | + } |
| 471 | + |
| 472 | + // lengths over 2^16 are not currently handled |
| 473 | + length = 0; |
| 474 | + bytesConsumed = 0; |
| 475 | + return false; |
| 476 | + } |
| 477 | + |
| 478 | + private static bool TryReadAsnInteger(ReadOnlySpan<byte> data, out ReadOnlySpan<byte> number, out int bytesConsumed) |
| 479 | + { |
| 480 | + // integer tag is 2 |
| 481 | + if (data[0] != 0x02) |
| 482 | + { |
| 483 | + number = default; |
| 484 | + bytesConsumed = 0; |
| 485 | + return false; |
| 486 | + } |
| 487 | + data = data.Slice(1); |
| 488 | + |
| 489 | + // tag is followed by the length of the integer |
| 490 | + if (!TryReadAsnLength(data, out var length, out var lengthBytesConsumed)) |
| 491 | + { |
| 492 | + number = default; |
| 493 | + bytesConsumed = 0; |
| 494 | + return false; |
| 495 | + } |
| 496 | + |
| 497 | + // length is followed by the integer bytes, MSB first |
| 498 | + number = data.Slice(lengthBytesConsumed, length); |
| 499 | + bytesConsumed = lengthBytesConsumed + length + 1; |
| 500 | + |
| 501 | + // trim leading zero bytes |
| 502 | + while (number.Length > 1 && number[0] == 0) |
| 503 | + number = number.Slice(1); |
| 504 | + |
| 505 | + return true; |
| 506 | + } |
| 507 | + |
| 508 | + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" |
| 509 | + private static ReadOnlySpan<byte> s_rsaOid => new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; |
432 | 510 | }
|
433 | 511 | }
|
0 commit comments