Skip to content

Commit 6bb5a7c

Browse files
authored
_ssl cleanup (#1389)
* _ssl cleanup * Update certificate * Fix some failing tests * Bump TLS version * Don't rebuild certificate chain * Enable more tests
1 parent 9566483 commit 6bb5a7c

6 files changed

Lines changed: 169 additions & 109 deletions

File tree

Src/IronPython.Modules/_ssl.cs

Lines changed: 104 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Net.Sockets;
1515
using System.Reflection;
1616
using System.Runtime.CompilerServices;
17+
using System.Runtime.InteropServices;
1718
using System.Security.Authentication;
1819
using System.Security.Cryptography;
1920
using System.Security.Cryptography.X509Certificates;
@@ -113,6 +114,7 @@ public static void RAND_add(object buf, double entropy) {
113114
public class _SSLContext {
114115
internal readonly X509Certificate2Collection _cert_store = new X509Certificate2Collection();
115116
internal string _cafile;
117+
internal X509Certificate2 _cert;
116118
private int _verify_mode = SSL_VERIFY_NONE;
117119

118120
public _SSLContext(CodeContext context, int protocol) {
@@ -148,6 +150,10 @@ public int verify_mode {
148150
if (_verify_mode != CERT_NONE && _verify_mode != CERT_OPTIONAL && _verify_mode != CERT_REQUIRED) {
149151
throw PythonOps.ValueError("invalid value for verify_mode");
150152
}
153+
// TODO: change this in 3.7
154+
if (check_hostname && value == CERT_NONE) {
155+
throw PythonOps.ValueError("Cannot set verify_mode to CERT_NONE when check_hostname is enabled.");
156+
}
151157
_verify_mode = value;
152158
}
153159
}
@@ -156,8 +162,16 @@ public int protocol {
156162
get; set;
157163
}
158164

165+
private bool _check_hostname;
159166
public bool check_hostname {
160-
get; set;
167+
get => _check_hostname;
168+
set {
169+
// TODO: change this in 3.7
170+
if (value && _verify_mode != CERT_OPTIONAL && _verify_mode != CERT_REQUIRED) {
171+
throw PythonOps.ValueError("check_hostname needs a SSL context with either CERT_OPTIONAL or CERT_REQUIRED");
172+
}
173+
_check_hostname = value;
174+
}
161175
}
162176

163177
public void set_default_verify_paths(CodeContext context) {
@@ -174,8 +188,19 @@ public void set_ecdh_curve(CodeContext context, [NotNull] Bytes curve) {
174188
throw PythonOps.ValueError($"unknown elliptic curve name {PythonOps.Repr(context, curve)}");
175189
}
176190

177-
public void load_cert_chain(string certfile, string keyfile = null, object password = null) {
191+
public void load_cert_chain(CodeContext context, string certfile, string keyfile = null, object password = null) {
192+
if (keyfile is not null) throw new NotImplementedException(nameof(keyfile));
193+
if (password is not null) throw new NotImplementedException(nameof(password));
194+
#if NET5_0_OR_GREATER
195+
_cert = X509Certificate2.CreateFromPemFile(certfile, keyfile);
196+
#else
197+
_cert = ReadCertificate(context, certfile, readKey: true);
198+
#endif
199+
}
178200

201+
public PythonList get_ca_certs(CodeContext context, bool binary_form = false) {
202+
if (binary_form) throw new NotImplementedException(nameof(binary_form));
203+
return new PythonList(_cert_store.Cast<X509Certificate2>().Select(c => CertificateToPython(context, c)));
179204
}
180205

181206
public void load_verify_locations(CodeContext context, object cafile = null, string capath = null, object cadata = null) {
@@ -199,23 +224,38 @@ public void load_verify_locations(CodeContext context, object cafile = null, str
199224
}
200225

201226
if (capath != null) {
227+
// TODO
202228
}
203229

204-
if (cadata != null) {
205-
var cabuf = cadata as IBufferProtocol;
206-
if (cabuf != null) {
207-
int pos = 0;
208-
byte[] contents;
209-
using (IPythonBuffer buf = cabuf.GetBuffer()) {
210-
contents = buf.AsReadOnlySpan().ToArray();
211-
}
212-
while (pos < contents.Length) {
213-
byte[] curr = new byte[contents.Length - pos];
214-
Array.Copy(contents, pos, curr, 0, contents.Length - pos);
215-
var cert = new X509Certificate2(curr);
230+
if (cadata is not null) {
231+
if (cadata is string s) {
232+
if (!StringOps.TryEncodeAscii(s, out Bytes ascii))
233+
throw PythonOps.ValueError("cadata should be an ASCII string or a bytes-like object");
234+
#if NET5_0_OR_GREATER
235+
_cert_store.ImportFromPem(s);
236+
#else
237+
string line;
238+
var lines = new List<string>();
239+
using var stream = new MemoryStream(ascii.UnsafeByteArray);
240+
using var sr = new StreamReader(stream);
241+
while ((line = sr.ReadLine()) != null)
242+
lines.Add(line);
243+
_cert_store.Add(ReadCertificate(context, string.Empty, lines.ToArray()));
244+
#endif
245+
} else if (cadata is IBufferProtocol cabuf) {
246+
using IPythonBuffer buf = cabuf.GetBuffer();
247+
var contents = buf.AsReadOnlySpan();
248+
while (contents.Length > 0) {
249+
#if NET5_0_OR_GREATER
250+
var cert = new X509Certificate2(contents);
251+
#else
252+
var cert = new X509Certificate2(contents.ToArray());
253+
#endif
216254
_cert_store.Add(cert);
217-
pos += cert.GetRawCertData().Length;
255+
contents = contents.Slice(cert.GetRawCertData().Length);
218256
}
257+
} else {
258+
throw PythonOps.ValueError("cadata should be an ASCII string or a bytes-like object");
219259
}
220260
}
221261
}
@@ -230,7 +270,6 @@ public class _SSLSocket {
230270
private SslStream _sslStream;
231271
private readonly PythonSocket.socket _socket;
232272
private readonly X509Certificate2Collection _certCollection;
233-
private readonly X509Certificate _cert;
234273
private readonly int _protocol, _certsMode;
235274
private readonly bool _validate, _serverSide;
236275
private readonly CodeContext _context;
@@ -276,10 +315,6 @@ internal _SSLSocket(CodeContext context, _SSLContext sslcontext, PythonSocket.so
276315
_certCollection = sslcontext._cert_store;
277316
}
278317

279-
if (sslcontext._cafile != null) {
280-
_cert = PythonSsl.ReadCertificate(context, sslcontext._cafile);
281-
}
282-
283318
_socket = sock;
284319

285320
EnsureSslStream(false);
@@ -357,36 +392,23 @@ internal bool CertValidationCallbackRequired(object sender, X509Certificate cert
357392
}
358393

359394
private void ValidateCertificate(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
360-
chain = new X509Chain();
361-
X509Certificate2Collection certificates = new X509Certificate2Collection();
362-
foreach (object cert in _certCollection) {
363-
if (cert is X509Certificate2) {
364-
certificates.Add((X509Certificate2)cert);
365-
} else if (cert is X509Certificate) {
366-
certificates.Add(new X509Certificate2((X509Certificate)cert));
367-
}
368-
}
369-
chain.ChainPolicy.ExtraStore.AddRange(certificates);
370-
chain.Build(new X509Certificate2(certificate));
371-
372-
if (chain.ChainStatus.Length > 0) {
373-
foreach (var elem in chain.ChainStatus) {
374-
if (elem.Status == X509ChainStatusFlags.UntrustedRoot) {
375-
bool isOk = false;
376-
foreach (var cert in _certCollection) {
377-
if (certificate.Issuer == cert.Subject) {
378-
isOk = true;
379-
}
380-
}
381-
382-
if (isOk) {
383-
continue;
395+
Debug.Assert(chain.ChainStatus.Length > 0);
396+
foreach (var elem in chain.ChainStatus) {
397+
if (elem.Status == X509ChainStatusFlags.UntrustedRoot) {
398+
bool isOk = false;
399+
foreach (var cert in _certCollection) {
400+
if (certificate.Issuer == cert.Subject) {
401+
isOk = true;
384402
}
385403
}
386404

387-
ValidationError(sslPolicyErrors);
388-
break;
405+
if (isOk) {
406+
continue;
407+
}
389408
}
409+
410+
ValidationError(sslPolicyErrors);
411+
break;
390412
}
391413
}
392414

@@ -411,15 +433,13 @@ public void do_handshake() {
411433

412434
try {
413435
if (_serverSide) {
436+
var _cert = context._cert;
437+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
438+
_cert = new X509Certificate2(_cert.Export(X509ContentType.Pkcs12));
439+
}
414440
_sslStream.AuthenticateAsServer(_cert, _certsMode == PythonSsl.CERT_REQUIRED, enabledSslProtocols, false);
415441
} else {
416-
417-
var collection = new X509CertificateCollection();
418-
419-
if (_cert != null) {
420-
collection.Add(_cert);
421-
}
422-
_sslStream.AuthenticateAsClient(_serverHostName ?? _socket._hostName, collection, enabledSslProtocols, false);
442+
_sslStream.AuthenticateAsClient(_serverHostName ?? _socket._hostName, context._cert_store, enabledSslProtocols, false);
423443
}
424444
} catch (AuthenticationException e) {
425445
((IDisposable)_socket._socket).Dispose();
@@ -500,6 +520,8 @@ public PythonTuple cipher() {
500520
return null;
501521
}
502522

523+
public object compression() => null; // TODO
524+
503525
private string ProtocolToPython() {
504526
switch (_sslStream.SslProtocol) {
505527
#pragma warning disable CA5397 // Do not use deprecated SslProtocols values
@@ -592,13 +614,20 @@ public string server() {
592614
[Documentation(@"Writes the bytes-like object b into the SSL object.
593615
594616
Returns the number of bytes written.")]
595-
public int write(CodeContext/*!*/ context, Bytes data) {
617+
public int write(CodeContext/*!*/ context, IBufferProtocol data) {
596618
EnsureSslStream(true);
597619

598-
byte[] buffer = data.UnsafeByteArray;
620+
using var buffer = data.GetBuffer();
599621
try {
600-
_sslStream.Write(buffer);
601-
return buffer.Length;
622+
#if NETCOREAPP
623+
var bytes = buffer.AsReadOnlySpan();
624+
_sslStream.Write(bytes);
625+
return bytes.Length;
626+
#else
627+
var bytes = buffer.AsUnsafeArray() ?? buffer.ToArray();
628+
_sslStream.Write(bytes);
629+
return bytes.Length;
630+
#endif
602631
} catch (Exception e) {
603632
throw PythonSocket.MakeException(context, e);
604633
}
@@ -921,6 +950,10 @@ private static X509Certificate2 ReadCertificate(CodeContext context, string file
921950
throw PythonExceptions.CreateThrowable(SSLError(context), "Can't open file ", filename);
922951
}
923952

953+
return ReadCertificate(context, filename, lines, readKey);
954+
}
955+
956+
private static X509Certificate2 ReadCertificate(CodeContext context, string filename, string[] lines, bool readKey = false) {
924957
X509Certificate2 cert = null;
925958
RSACryptoServiceProvider key = null;
926959
try {
@@ -945,6 +978,16 @@ private static X509Certificate2 ReadCertificate(CodeContext context, string file
945978
throw ErrorDecoding(context, filename, e);
946979
}
947980
}
981+
} else if (lines[i] == "-----BEGIN PRIVATE KEY-----") {
982+
var keyStr = ReadToEnd(lines, ref i, "-----END PRIVATE KEY-----");
983+
if (readKey) {
984+
try {
985+
var keyBytes = Convert.FromBase64String(keyStr.ToString());
986+
// TODO
987+
} catch (Exception e) {
988+
throw ErrorDecoding(context, filename, e);
989+
}
990+
}
948991
}
949992
}
950993
} catch (InvalidOperationException e) {
@@ -981,7 +1024,6 @@ private static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certifi
9811024
}
9821025
#endif
9831026

984-
9851027
#region Private Key Parsing
9861028

9871029
private const int ClassOffset = 6;
@@ -1027,7 +1069,7 @@ private static RSACryptoServiceProvider ParsePkcs1DerEncodedPrivateKey(CodeConte
10271069
int version = ReadUniversalInt(x, ref offset);
10281070
if (version != 0) {
10291071
// unsupported version
1030-
throw new InvalidOperationException(String.Format("bad vesion: {0}", version));
1072+
throw new InvalidOperationException(String.Format("bad version: {0}", version));
10311073
}
10321074

10331075
// read in parameters and initialize provider
@@ -1071,7 +1113,7 @@ private static byte[] ReadUniversalIntAsBytes(byte[] x, ref int offset) {
10711113
private static void ReadIntType(byte[] x, ref int offset) {
10721114
int versionType = x[offset++];
10731115
if (versionType != UniversalInteger) {
1074-
throw new InvalidOperationException(String.Format("expected version, fonud {0}", versionType));
1116+
throw new InvalidOperationException(String.Format("expected version, found {0}", versionType));
10751117
}
10761118
}
10771119
private static int ReadUniversalInt(byte[] x, ref int offset) {
@@ -1134,8 +1176,8 @@ private static Exception ErrorDecoding(CodeContext context, params object[] args
11341176
public const int CERT_OPTIONAL = 1;
11351177
public const int CERT_REQUIRED = 2;
11361178

1137-
public const int PROTOCOL_SSLv2 = 0;
1138-
public const int PROTOCOL_SSLv3 = 1;
1179+
private const int PROTOCOL_SSLv2 = 0;
1180+
private const int PROTOCOL_SSLv3 = 1;
11391181
public const int PROTOCOL_SSLv23 = 2;
11401182
public const int PROTOCOL_TLSv1 = 3;
11411183
public const int PROTOCOL_TLSv1_1 = 4;
Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,34 @@
11
-----BEGIN CERTIFICATE-----
2-
MIIClTCCAf6gAwIBAgIJAKGU95wKR8pTMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV
3-
BAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEjMCEGA1UECgwaUHl0aG9u
4-
IFNvZnR3YXJlIEZvdW5kYXRpb24xIzAhBgNVBAMMGnNlbGYtc2lnbmVkLnB5dGhv
5-
bnRlc3QubmV0MB4XDTE0MTEwMjE4MDkyOVoXDTI0MTAzMDE4MDkyOVowcDELMAkG
6-
A1UEBhMCWFkxFzAVBgNVBAcMDkNhc3RsZSBBbnRocmF4MSMwIQYDVQQKDBpQeXRo
7-
b24gU29mdHdhcmUgRm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0
8-
aG9udGVzdC5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANDXQXW9tjyZ
9-
Xt0Iv2tLL1+jinr4wGg36ioLDLFkMf+2Y1GL0v0BnKYG4N1OKlAU15LXGeGer8vm
10-
Sv/yIvmdrELvhAbbo3w4a9TMYQA4XkIVLdvu3mvNOAet+8PMJxn26dbDhG809ALv
11-
EHY57lQsBS3G59RZyBPVqAqmImWNJnVzAgMBAAGjNzA1MCUGA1UdEQQeMByCGnNl
12-
bGYtc2lnbmVkLnB5dGhvbnRlc3QubmV0MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
13-
AQEFBQADgYEAIuzAhgMouJpNdf3URCHIineyoSt6WK/9+eyUcjlKOrDoXNZaD72h
14-
TXMeKYoWvJyVcSLKL8ckPtDobgP2OTt0UkyAaj0n+ZHaqq1lH2yVfGUA1ILJv515
15-
C8BqbvVZuqm3i7ygmw3bqE/lYMgOrYtXXnqOrz6nvsE6Yc9V9rFflOM=
2+
MIIF9zCCA9+gAwIBAgIUH98b4Fw/DyugC9cV7VK7ZODzHsIwDQYJKoZIhvcNAQEL
3+
BQAwgYoxCzAJBgNVBAYTAlhZMRcwFQYDVQQIDA5DYXN0bGUgQW50aHJheDEYMBYG
4+
A1UEBwwPQXJndW1lbnQgQ2xpbmljMSMwIQYDVQQKDBpQeXRob24gU29mdHdhcmUg
5+
Rm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0aG9udGVzdC5uZXQw
6+
HhcNMTkwNTA4MDEwMjQzWhcNMjcwNzI0MDEwMjQzWjCBijELMAkGA1UEBhMCWFkx
7+
FzAVBgNVBAgMDkNhc3RsZSBBbnRocmF4MRgwFgYDVQQHDA9Bcmd1bWVudCBDbGlu
8+
aWMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMSMwIQYDVQQD
9+
DBpzZWxmLXNpZ25lZC5weXRob250ZXN0Lm5ldDCCAiIwDQYJKoZIhvcNAQEBBQAD
10+
ggIPADCCAgoCggIBAMKdJlyCThkahwoBb7pl5q64Pe9Fn5jrIvzsveHTc97TpjV2
11+
RLfICnXKrltPk/ohkVl6K5SUZQZwMVzFubkyxE0nZPHYHlpiKWQxbsYVkYv01rix
12+
IFdLvaxxbGYke2jwQao31s4o61AdlsfK1SdpHQUynBBMssqI3SB4XPmcA7e+wEEx
13+
jxjVish4ixA1vuIZOx8yibu+CFCf/geEjoBMF3QPdzULzlrCSw8k/45iZCSoNbvK
14+
DoL4TVV07PHOxpheDh8ZQmepGvU6pVqhb9m4lgmV0OGWHgozd5Ur9CbTVDmxIEz3
15+
TSoRtNJK7qtyZdGNqwjksQxgZTjM/d/Lm/BJG99AiOmYOjsl9gbQMZgvQmMAtUsI
16+
aMJnQuZ6R+KEpW/TR5qSKLWZSG45z/op+tzI2m+cE6HwTRVAWbcuJxcAA55MZjqU
17+
OOOu3BBYMjS5nf2sQ9uoXsVBFH7i0mQqoW1SLzr9opI8KsWwFxQmO2vBxWYaN+lH
18+
OmwBZBwyODIsmI1YGXmTp09NxRYz3Qe5GCgFzYowpMrcxUC24iduIdMwwhRM7rKg
19+
7GtIWMSrFfuI1XCLRmSlhDbhNN6fVg2f8Bo9PdH9ihiIyxSrc+FOUasUYCCJvlSZ
20+
8hFUlLvcmrZlWuazohm0lsXuMK1JflmQr/DA/uXxP9xzFfRy+RU3jDyxJbRHAgMB
21+
AAGjUzBRMB0GA1UdDgQWBBSQJyxiPMRK01i+0BsV9zUwDiBaHzAfBgNVHSMEGDAW
22+
gBSQJyxiPMRK01i+0BsV9zUwDiBaHzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
23+
DQEBCwUAA4ICAQCR+7a7N/m+WLkxPPIA/CB4MOr2Uf8ixTv435Nyv6rXOun0+lTP
24+
ExSZ0uYQ+L0WylItI3cQHULldDueD+s8TGzxf5woaLKf6tqyr0NYhKs+UeNEzDnN
25+
9PHQIhX0SZw3XyXGUgPNBfRCg2ZDdtMMdOU4XlQN/IN/9hbYTrueyY7eXq9hmtI9
26+
1srftAMqr9SR1JP7aHI6DVgrEsZVMTDnfT8WmLSGLlY1HmGfdEn1Ip5sbo9uSkiH
27+
AEPgPfjYIvR5LqTOMn4KsrlZyBbFIDh9Sl99M1kZzgH6zUGVLCDg1y6Cms69fx/e
28+
W1HoIeVkY4b4TY7Bk7JsqyNhIuqu7ARaxkdaZWhYaA2YyknwANdFfNpfH+elCLIk
29+
BUt5S3f4i7DaUePTvKukCZiCq4Oyln7RcOn5If73wCeLB/ZM9Ei1HforyLWP1CN8
30+
XLfpHaoeoPSWIveI0XHUl65LsPN2UbMbul/F23hwl+h8+BLmyAS680Yhn4zEN6Ku
31+
B7Po90HoFa1Du3bmx4jsN73UkT/dwMTi6K072FbipnC1904oGlWmLwvAHvrtxxmL
32+
Pl3pvEaZIu8wa/PNF6Y7J7VIewikIJq6Ta6FrWeFfzMWOj2qA1ZZi6fUaDSNYvuV
33+
J5quYKCc/O+I/yDDf8wyBbZ/gvUXzUHTMYGG+bFrn1p7XDbYYeEJ6R/xEg==
1634
-----END CERTIFICATE-----

Src/StdLib/Lib/test/test_httplib.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ def test_networked_good_cert(self):
956956
import ssl
957957
support.requires('network')
958958
with support.transient_internet('self-signed.pythontest.net'):
959-
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
959+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
960960
context.verify_mode = ssl.CERT_REQUIRED
961961
context.load_verify_locations(CERT_selfsigned_pythontestdotnet)
962962
h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
@@ -971,7 +971,7 @@ def test_networked_bad_cert(self):
971971
import ssl
972972
support.requires('network')
973973
with support.transient_internet('self-signed.pythontest.net'):
974-
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
974+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
975975
context.verify_mode = ssl.CERT_REQUIRED
976976
context.load_verify_locations(CERT_localhost)
977977
h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context)

Tests/modules/network_related/test__ssl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ def test_constants(self):
2828
self.assertEqual(_ssl.CERT_NONE, 0)
2929
self.assertEqual(_ssl.CERT_OPTIONAL, 1)
3030
self.assertEqual(_ssl.CERT_REQUIRED, 2)
31-
if sys.version_info >= (3,5):
31+
if is_cli or sys.version_info >= (3,5):
3232
self.assertRaises(AttributeError, lambda: _ssl.PROTOCOL_SSLv2)
3333
else:
3434
self.assertEqual(_ssl.PROTOCOL_SSLv2, 0)
3535
self.assertEqual(_ssl.PROTOCOL_SSLv23, 2)
36-
if sys.version_info >= (3,7):
36+
if is_cli or sys.version_info >= (3,7):
3737
self.assertRaises(AttributeError, lambda: _ssl.PROTOCOL_SSLv3)
3838
else:
3939
self.assertEqual(_ssl.PROTOCOL_SSLv3, 1)

0 commit comments

Comments
 (0)