Skip to content

Commit bdc774f

Browse files
committed
Load a RSA private key from a PEM file.
1 parent 3c745ed commit bdc774f

File tree

2 files changed

+235
-84
lines changed

2 files changed

+235
-84
lines changed

src/MySqlConnector/Utilities/Utility.cs

Lines changed: 162 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Buffers.Text;
33
using System.Globalization;
44
using System.IO;
5-
using System.Linq;
65
using System.Net;
76
using System.Runtime.InteropServices;
87
using System.Reflection;
@@ -68,106 +67,119 @@ public static unsafe void Convert(this Encoder encoder, ReadOnlySpan<char> chars
6867
#endif
6968

7069
/// <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.
7271
/// </summary>
7372
/// <param name="publicKey">The public key, in PEM format.</param>
7473
/// <returns>An RSA public key, or <c>null</c> on failure.</returns>
7574
public static RSA DecodeX509PublicKey(string publicKey)
7675
{
7776
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);
7890

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);
8194

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)
8596
{
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+
}
98130

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
138142
{
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+
}
141147

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);
143151

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);
146155

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);
153159

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);
155163

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);
160167

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+
};
171183
}
172184

173185
/// <summary>
@@ -429,5 +441,71 @@ public static SslProtocols GetDefaultSslProtocols()
429441
#else
430442
public static SslProtocols GetDefaultSslProtocols() => SslProtocols.None;
431443
#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 };
432510
}
433511
}

0 commit comments

Comments
 (0)