Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 104 additions & 62 deletions Src/IronPython.Modules/_ssl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
Expand Down Expand Up @@ -113,6 +114,7 @@ public static void RAND_add(object buf, double entropy) {
public class _SSLContext {
internal readonly X509Certificate2Collection _cert_store = new X509Certificate2Collection();
internal string _cafile;
internal X509Certificate2 _cert;
private int _verify_mode = SSL_VERIFY_NONE;

public _SSLContext(CodeContext context, int protocol) {
Expand Down Expand Up @@ -148,6 +150,10 @@ public int verify_mode {
if (_verify_mode != CERT_NONE && _verify_mode != CERT_OPTIONAL && _verify_mode != CERT_REQUIRED) {
throw PythonOps.ValueError("invalid value for verify_mode");
}
// TODO: change this in 3.7
if (check_hostname && value == CERT_NONE) {
throw PythonOps.ValueError("Cannot set verify_mode to CERT_NONE when check_hostname is enabled.");
}
_verify_mode = value;
}
}
Expand All @@ -156,8 +162,16 @@ public int protocol {
get; set;
}

private bool _check_hostname;
public bool check_hostname {
get; set;
get => _check_hostname;
set {
// TODO: change this in 3.7
if (value && _verify_mode != CERT_OPTIONAL && _verify_mode != CERT_REQUIRED) {
throw PythonOps.ValueError("check_hostname needs a SSL context with either CERT_OPTIONAL or CERT_REQUIRED");
}
_check_hostname = value;
}
}

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

public void load_cert_chain(string certfile, string keyfile = null, object password = null) {
public void load_cert_chain(CodeContext context, string certfile, string keyfile = null, object password = null) {
if (keyfile is not null) throw new NotImplementedException(nameof(keyfile));
if (password is not null) throw new NotImplementedException(nameof(password));
#if NET5_0_OR_GREATER
_cert = X509Certificate2.CreateFromPemFile(certfile, keyfile);
#else
_cert = ReadCertificate(context, certfile, readKey: true);
#endif
}

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

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

if (capath != null) {
// TODO
}

if (cadata != null) {
var cabuf = cadata as IBufferProtocol;
if (cabuf != null) {
int pos = 0;
byte[] contents;
using (IPythonBuffer buf = cabuf.GetBuffer()) {
contents = buf.AsReadOnlySpan().ToArray();
}
while (pos < contents.Length) {
byte[] curr = new byte[contents.Length - pos];
Array.Copy(contents, pos, curr, 0, contents.Length - pos);
var cert = new X509Certificate2(curr);
if (cadata is not null) {
if (cadata is string s) {
if (!StringOps.TryEncodeAscii(s, out Bytes ascii))
throw PythonOps.ValueError("cadata should be an ASCII string or a bytes-like object");
#if NET5_0_OR_GREATER
_cert_store.ImportFromPem(s);
#else
string line;
var lines = new List<string>();
using var stream = new MemoryStream(ascii.UnsafeByteArray);
using var sr = new StreamReader(stream);
while ((line = sr.ReadLine()) != null)
lines.Add(line);
_cert_store.Add(ReadCertificate(context, string.Empty, lines.ToArray()));
#endif
} else if (cadata is IBufferProtocol cabuf) {
using IPythonBuffer buf = cabuf.GetBuffer();
var contents = buf.AsReadOnlySpan();
while (contents.Length > 0) {
#if NET5_0_OR_GREATER
var cert = new X509Certificate2(contents);
#else
var cert = new X509Certificate2(contents.ToArray());
#endif
_cert_store.Add(cert);
pos += cert.GetRawCertData().Length;
contents = contents.Slice(cert.GetRawCertData().Length);
}
} else {
throw PythonOps.ValueError("cadata should be an ASCII string or a bytes-like object");
}
}
}
Expand All @@ -230,7 +270,6 @@ public class _SSLSocket {
private SslStream _sslStream;
private readonly PythonSocket.socket _socket;
private readonly X509Certificate2Collection _certCollection;
private readonly X509Certificate _cert;
private readonly int _protocol, _certsMode;
private readonly bool _validate, _serverSide;
private readonly CodeContext _context;
Expand Down Expand Up @@ -276,10 +315,6 @@ internal _SSLSocket(CodeContext context, _SSLContext sslcontext, PythonSocket.so
_certCollection = sslcontext._cert_store;
}

if (sslcontext._cafile != null) {
_cert = PythonSsl.ReadCertificate(context, sslcontext._cafile);
}

_socket = sock;

EnsureSslStream(false);
Expand Down Expand Up @@ -357,36 +392,23 @@ internal bool CertValidationCallbackRequired(object sender, X509Certificate cert
}

private void ValidateCertificate(X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
chain = new X509Chain();
X509Certificate2Collection certificates = new X509Certificate2Collection();
foreach (object cert in _certCollection) {
if (cert is X509Certificate2) {
certificates.Add((X509Certificate2)cert);
} else if (cert is X509Certificate) {
certificates.Add(new X509Certificate2((X509Certificate)cert));
}
}
chain.ChainPolicy.ExtraStore.AddRange(certificates);
chain.Build(new X509Certificate2(certificate));

if (chain.ChainStatus.Length > 0) {
foreach (var elem in chain.ChainStatus) {
if (elem.Status == X509ChainStatusFlags.UntrustedRoot) {
bool isOk = false;
foreach (var cert in _certCollection) {
if (certificate.Issuer == cert.Subject) {
isOk = true;
}
}

if (isOk) {
continue;
Debug.Assert(chain.ChainStatus.Length > 0);
foreach (var elem in chain.ChainStatus) {
if (elem.Status == X509ChainStatusFlags.UntrustedRoot) {
bool isOk = false;
foreach (var cert in _certCollection) {
if (certificate.Issuer == cert.Subject) {
isOk = true;
}
}

ValidationError(sslPolicyErrors);
break;
if (isOk) {
continue;
}
}

ValidationError(sslPolicyErrors);
break;
}
}

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

try {
if (_serverSide) {
var _cert = context._cert;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
_cert = new X509Certificate2(_cert.Export(X509ContentType.Pkcs12));
}
_sslStream.AuthenticateAsServer(_cert, _certsMode == PythonSsl.CERT_REQUIRED, enabledSslProtocols, false);
} else {

var collection = new X509CertificateCollection();

if (_cert != null) {
collection.Add(_cert);
}
_sslStream.AuthenticateAsClient(_serverHostName ?? _socket._hostName, collection, enabledSslProtocols, false);
_sslStream.AuthenticateAsClient(_serverHostName ?? _socket._hostName, context._cert_store, enabledSslProtocols, false);
}
} catch (AuthenticationException e) {
((IDisposable)_socket._socket).Dispose();
Expand Down Expand Up @@ -500,6 +520,8 @@ public PythonTuple cipher() {
return null;
}

public object compression() => null; // TODO

private string ProtocolToPython() {
switch (_sslStream.SslProtocol) {
#pragma warning disable CA5397 // Do not use deprecated SslProtocols values
Expand Down Expand Up @@ -592,13 +614,20 @@ public string server() {
[Documentation(@"Writes the bytes-like object b into the SSL object.

Returns the number of bytes written.")]
public int write(CodeContext/*!*/ context, Bytes data) {
public int write(CodeContext/*!*/ context, IBufferProtocol data) {
EnsureSslStream(true);

byte[] buffer = data.UnsafeByteArray;
using var buffer = data.GetBuffer();
try {
_sslStream.Write(buffer);
return buffer.Length;
#if NETCOREAPP
var bytes = buffer.AsReadOnlySpan();
_sslStream.Write(bytes);
return bytes.Length;
#else
var bytes = buffer.AsUnsafeArray() ?? buffer.ToArray();
_sslStream.Write(bytes);
return bytes.Length;
#endif
} catch (Exception e) {
throw PythonSocket.MakeException(context, e);
}
Expand Down Expand Up @@ -921,6 +950,10 @@ private static X509Certificate2 ReadCertificate(CodeContext context, string file
throw PythonExceptions.CreateThrowable(SSLError(context), "Can't open file ", filename);
}

return ReadCertificate(context, filename, lines, readKey);
}

private static X509Certificate2 ReadCertificate(CodeContext context, string filename, string[] lines, bool readKey = false) {
X509Certificate2 cert = null;
RSACryptoServiceProvider key = null;
try {
Expand All @@ -945,6 +978,16 @@ private static X509Certificate2 ReadCertificate(CodeContext context, string file
throw ErrorDecoding(context, filename, e);
}
}
} else if (lines[i] == "-----BEGIN PRIVATE KEY-----") {
var keyStr = ReadToEnd(lines, ref i, "-----END PRIVATE KEY-----");
if (readKey) {
try {
var keyBytes = Convert.FromBase64String(keyStr.ToString());
// TODO
} catch (Exception e) {
throw ErrorDecoding(context, filename, e);
}
}
}
}
} catch (InvalidOperationException e) {
Expand Down Expand Up @@ -981,7 +1024,6 @@ private static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certifi
}
#endif


#region Private Key Parsing

private const int ClassOffset = 6;
Expand Down Expand Up @@ -1027,7 +1069,7 @@ private static RSACryptoServiceProvider ParsePkcs1DerEncodedPrivateKey(CodeConte
int version = ReadUniversalInt(x, ref offset);
if (version != 0) {
// unsupported version
throw new InvalidOperationException(String.Format("bad vesion: {0}", version));
throw new InvalidOperationException(String.Format("bad version: {0}", version));
}

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

public const int PROTOCOL_SSLv2 = 0;
public const int PROTOCOL_SSLv3 = 1;
private const int PROTOCOL_SSLv2 = 0;
private const int PROTOCOL_SSLv3 = 1;
public const int PROTOCOL_SSLv23 = 2;
public const int PROTOCOL_TLSv1 = 3;
public const int PROTOCOL_TLSv1_1 = 4;
Expand Down
46 changes: 32 additions & 14 deletions Src/StdLib/Lib/test/selfsigned_pythontestdotnet.pem
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIClTCCAf6gAwIBAgIJAKGU95wKR8pTMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV
BAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEjMCEGA1UECgwaUHl0aG9u
IFNvZnR3YXJlIEZvdW5kYXRpb24xIzAhBgNVBAMMGnNlbGYtc2lnbmVkLnB5dGhv
bnRlc3QubmV0MB4XDTE0MTEwMjE4MDkyOVoXDTI0MTAzMDE4MDkyOVowcDELMAkG
A1UEBhMCWFkxFzAVBgNVBAcMDkNhc3RsZSBBbnRocmF4MSMwIQYDVQQKDBpQeXRo
b24gU29mdHdhcmUgRm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0
aG9udGVzdC5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANDXQXW9tjyZ
Xt0Iv2tLL1+jinr4wGg36ioLDLFkMf+2Y1GL0v0BnKYG4N1OKlAU15LXGeGer8vm
Sv/yIvmdrELvhAbbo3w4a9TMYQA4XkIVLdvu3mvNOAet+8PMJxn26dbDhG809ALv
EHY57lQsBS3G59RZyBPVqAqmImWNJnVzAgMBAAGjNzA1MCUGA1UdEQQeMByCGnNl
bGYtc2lnbmVkLnB5dGhvbnRlc3QubmV0MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
AQEFBQADgYEAIuzAhgMouJpNdf3URCHIineyoSt6WK/9+eyUcjlKOrDoXNZaD72h
TXMeKYoWvJyVcSLKL8ckPtDobgP2OTt0UkyAaj0n+ZHaqq1lH2yVfGUA1ILJv515
C8BqbvVZuqm3i7ygmw3bqE/lYMgOrYtXXnqOrz6nvsE6Yc9V9rFflOM=
MIIF9zCCA9+gAwIBAgIUH98b4Fw/DyugC9cV7VK7ZODzHsIwDQYJKoZIhvcNAQEL
BQAwgYoxCzAJBgNVBAYTAlhZMRcwFQYDVQQIDA5DYXN0bGUgQW50aHJheDEYMBYG
A1UEBwwPQXJndW1lbnQgQ2xpbmljMSMwIQYDVQQKDBpQeXRob24gU29mdHdhcmUg
Rm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0aG9udGVzdC5uZXQw
HhcNMTkwNTA4MDEwMjQzWhcNMjcwNzI0MDEwMjQzWjCBijELMAkGA1UEBhMCWFkx
FzAVBgNVBAgMDkNhc3RsZSBBbnRocmF4MRgwFgYDVQQHDA9Bcmd1bWVudCBDbGlu
aWMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMSMwIQYDVQQD
DBpzZWxmLXNpZ25lZC5weXRob250ZXN0Lm5ldDCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBAMKdJlyCThkahwoBb7pl5q64Pe9Fn5jrIvzsveHTc97TpjV2
RLfICnXKrltPk/ohkVl6K5SUZQZwMVzFubkyxE0nZPHYHlpiKWQxbsYVkYv01rix
IFdLvaxxbGYke2jwQao31s4o61AdlsfK1SdpHQUynBBMssqI3SB4XPmcA7e+wEEx
jxjVish4ixA1vuIZOx8yibu+CFCf/geEjoBMF3QPdzULzlrCSw8k/45iZCSoNbvK
DoL4TVV07PHOxpheDh8ZQmepGvU6pVqhb9m4lgmV0OGWHgozd5Ur9CbTVDmxIEz3
TSoRtNJK7qtyZdGNqwjksQxgZTjM/d/Lm/BJG99AiOmYOjsl9gbQMZgvQmMAtUsI
aMJnQuZ6R+KEpW/TR5qSKLWZSG45z/op+tzI2m+cE6HwTRVAWbcuJxcAA55MZjqU
OOOu3BBYMjS5nf2sQ9uoXsVBFH7i0mQqoW1SLzr9opI8KsWwFxQmO2vBxWYaN+lH
OmwBZBwyODIsmI1YGXmTp09NxRYz3Qe5GCgFzYowpMrcxUC24iduIdMwwhRM7rKg
7GtIWMSrFfuI1XCLRmSlhDbhNN6fVg2f8Bo9PdH9ihiIyxSrc+FOUasUYCCJvlSZ
8hFUlLvcmrZlWuazohm0lsXuMK1JflmQr/DA/uXxP9xzFfRy+RU3jDyxJbRHAgMB
AAGjUzBRMB0GA1UdDgQWBBSQJyxiPMRK01i+0BsV9zUwDiBaHzAfBgNVHSMEGDAW
gBSQJyxiPMRK01i+0BsV9zUwDiBaHzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4ICAQCR+7a7N/m+WLkxPPIA/CB4MOr2Uf8ixTv435Nyv6rXOun0+lTP
ExSZ0uYQ+L0WylItI3cQHULldDueD+s8TGzxf5woaLKf6tqyr0NYhKs+UeNEzDnN
9PHQIhX0SZw3XyXGUgPNBfRCg2ZDdtMMdOU4XlQN/IN/9hbYTrueyY7eXq9hmtI9
1srftAMqr9SR1JP7aHI6DVgrEsZVMTDnfT8WmLSGLlY1HmGfdEn1Ip5sbo9uSkiH
AEPgPfjYIvR5LqTOMn4KsrlZyBbFIDh9Sl99M1kZzgH6zUGVLCDg1y6Cms69fx/e
W1HoIeVkY4b4TY7Bk7JsqyNhIuqu7ARaxkdaZWhYaA2YyknwANdFfNpfH+elCLIk
BUt5S3f4i7DaUePTvKukCZiCq4Oyln7RcOn5If73wCeLB/ZM9Ei1HforyLWP1CN8
XLfpHaoeoPSWIveI0XHUl65LsPN2UbMbul/F23hwl+h8+BLmyAS680Yhn4zEN6Ku
B7Po90HoFa1Du3bmx4jsN73UkT/dwMTi6K072FbipnC1904oGlWmLwvAHvrtxxmL
Pl3pvEaZIu8wa/PNF6Y7J7VIewikIJq6Ta6FrWeFfzMWOj2qA1ZZi6fUaDSNYvuV
J5quYKCc/O+I/yDDf8wyBbZ/gvUXzUHTMYGG+bFrn1p7XDbYYeEJ6R/xEg==
-----END CERTIFICATE-----
4 changes: 2 additions & 2 deletions Src/StdLib/Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ def test_networked_good_cert(self):
import ssl
support.requires('network')
with support.transient_internet('self-signed.pythontest.net'):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CERT_selfsigned_pythontestdotnet)
h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
Expand All @@ -971,7 +971,7 @@ def test_networked_bad_cert(self):
import ssl
support.requires('network')
with support.transient_internet('self-signed.pythontest.net'):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CERT_localhost)
h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
Expand Down
4 changes: 2 additions & 2 deletions Tests/modules/network_related/test__ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def test_constants(self):
self.assertEqual(_ssl.CERT_NONE, 0)
self.assertEqual(_ssl.CERT_OPTIONAL, 1)
self.assertEqual(_ssl.CERT_REQUIRED, 2)
if sys.version_info >= (3,5):
if is_cli or sys.version_info >= (3,5):
self.assertRaises(AttributeError, lambda: _ssl.PROTOCOL_SSLv2)
else:
self.assertEqual(_ssl.PROTOCOL_SSLv2, 0)
self.assertEqual(_ssl.PROTOCOL_SSLv23, 2)
if sys.version_info >= (3,7):
if is_cli or sys.version_info >= (3,7):
self.assertRaises(AttributeError, lambda: _ssl.PROTOCOL_SSLv3)
else:
self.assertEqual(_ssl.PROTOCOL_SSLv3, 1)
Expand Down
Loading