< Summary

Information
Class: Ice.SSL.SSLEngine
Assembly: Ice
File(s): /_/csharp/src/Ice/SSL/SSLEngine.cs
Tag: 91_21789722663
Line coverage
76%
Covered lines: 188
Uncovered lines: 59
Coverable lines: 247
Total lines: 542
Line coverage: 76.1%
Branch coverage
63%
Covered branches: 102
Total branches: 160
Branch coverage: 63.7%
Method coverage
81%
Covered methods: 13
Total methods: 16
Method coverage: 81.2%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
initialize()76.92%47.292668.42%
useMachineContext()100%210%
caCerts()100%210%
communicator()100%11100%
securityTraceLevel()100%11100%
securityTraceCategory()100%11100%
certs()100%11100%
traceStream(...)0%110100%
verifyPeer(...)83.33%6.11685.71%
createClientAuthenticationOptions(...)83.33%12.031294.12%
createServerAuthenticationOptions(...)70%10.251086.36%
isAbsolutePath(...)10%94.622042.86%
findCertificates(...)90.38%55.715288.89%
createSecureString(...)100%22100%
checkPath(...)50%9875%

File(s)

/_/csharp/src/Ice/SSL/SSLEngine.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using Ice.Internal;
 4using System.Diagnostics;
 5using System.Net.Security;
 6using System.Security;
 7using System.Security.Cryptography;
 8using System.Security.Cryptography.X509Certificates;
 9using System.Text;
 10
 11namespace Ice.SSL;
 12
 13internal class SSLEngine
 14{
 115    internal SSLEngine(Ice.Communicator communicator)
 16    {
 117        _communicator = communicator;
 118        _logger = communicator.getLogger();
 119        _securityTraceLevel = _communicator.getProperties().getIcePropertyAsInt("IceSSL.Trace.Security");
 120        _securityTraceCategory = "Security";
 121        _trustManager = new TrustManager(_communicator);
 122    }
 23
 24    internal void initialize()
 25    {
 126        Ice.Properties properties = communicator().getProperties();
 27
 28        // Check for a default directory. We look in this directory for files mentioned in the configuration.
 129        _defaultDir = properties.getIceProperty("IceSSL.DefaultDir");
 30
 131        _verifyPeer = properties.getIcePropertyAsInt("IceSSL.VerifyPeer");
 32
 33        // CheckCRL determines whether the certificate revocation list is checked, and how strictly.
 134        _checkCRL = properties.getIcePropertyAsInt("IceSSL.CheckCRL");
 35
 136        string certStoreLocation = properties.getIceProperty("IceSSL.CertStoreLocation");
 37        StoreLocation storeLocation;
 138        if (certStoreLocation == "CurrentUser")
 39        {
 140            storeLocation = StoreLocation.CurrentUser;
 41        }
 042        else if (certStoreLocation == "LocalMachine")
 43        {
 044            storeLocation = StoreLocation.LocalMachine;
 45        }
 46        else
 47        {
 048            _logger.warning(
 049                "Invalid IceSSL.CertStoreLocation value `" + certStoreLocation + "' adjusted to `CurrentUser'");
 050            storeLocation = StoreLocation.CurrentUser;
 51        }
 152        _useMachineContext = certStoreLocation == "LocalMachine";
 53
 54        // CheckCertName determines whether we compare the name in a peer's certificate against its hostname.
 155        _checkCertName = properties.getIcePropertyAsInt("IceSSL.CheckCertName") > 0;
 56
 57        Debug.Assert(_certs == null);
 58        // If IceSSL.CertFile is defined, load a certificate from a file and add it to the collection.
 159        _certs = [];
 160        string certFile = properties.getIceProperty("IceSSL.CertFile");
 161        string passwordStr = properties.getIceProperty("IceSSL.Password");
 162        string findCert = properties.getIceProperty("IceSSL.FindCert");
 63
 164        if (certFile.Length > 0)
 65        {
 166            if (!checkPath(ref certFile))
 67            {
 068                throw new Ice.InitializationException($"IceSSL: certificate file not found: {certFile}");
 69            }
 70
 71            try
 72            {
 73                X509Certificate2 cert;
 74                X509KeyStorageFlags importFlags;
 175                if (_useMachineContext)
 76                {
 077                    importFlags = X509KeyStorageFlags.MachineKeySet;
 78                }
 79                else
 80                {
 181                    importFlags = X509KeyStorageFlags.UserKeySet;
 82                }
 83
 184                if (passwordStr.Length > 0)
 85                {
 186                    using SecureString password = createSecureString(passwordStr);
 187                    cert = new X509Certificate2(certFile, password, importFlags);
 88                }
 89                else
 90                {
 191                    cert = new X509Certificate2(certFile, (string)null, importFlags);
 92                }
 193                _certs.Add(cert);
 194            }
 095            catch (CryptographicException ex)
 96            {
 097                throw new Ice.InitializationException(
 098                    $"IceSSL: error while attempting to load certificate from {certFile}",
 099                    ex);
 100            }
 101        }
 1102        else if (findCert.Length > 0)
 103        {
 1104            string certStore = properties.getIceProperty("IceSSL.CertStore");
 1105            _certs.AddRange(findCertificates("IceSSL.FindCert", storeLocation, certStore, findCert));
 1106            if (_certs.Count == 0)
 107            {
 1108                throw new Ice.InitializationException("IceSSL: no certificates found");
 109            }
 110        }
 111
 112        Debug.Assert(_caCerts == null);
 1113        string certAuthFile = properties.getIceProperty("IceSSL.CAs");
 1114        if (certAuthFile.Length > 0 || properties.getIcePropertyAsInt("IceSSL.UsePlatformCAs") <= 0)
 115        {
 1116            _caCerts = [];
 117        }
 118
 1119        if (certAuthFile.Length > 0)
 120        {
 1121            if (!checkPath(ref certAuthFile))
 122            {
 0123                throw new Ice.InitializationException($"IceSSL: CA certificate file not found: {certAuthFile}");
 124            }
 125            try
 126            {
 127                try
 128                {
 129                    // First try to import as a PEM file, which supports importing multiple certificates from a PEM
 130                    // encoded file
 1131                    _caCerts.ImportFromPemFile(certAuthFile);
 1132                }
 0133                catch (CryptographicException)
 134                {
 135                    // Expected if the file is not in PEM format.
 0136                }
 137
 1138                if (_caCerts.Count == 0)
 139                {
 140                    // Fallback to Import which handles DER/PFX.
 1141                    _caCerts.Import(certAuthFile);
 142                }
 1143            }
 0144            catch (Exception ex)
 145            {
 0146                throw new Ice.InitializationException(
 0147                    $"IceSSL: error while attempting to load CA certificate from {certAuthFile}",
 0148                    ex);
 149            }
 150        }
 1151    }
 152
 0153    internal bool useMachineContext() => _useMachineContext;
 154
 0155    internal X509Certificate2Collection caCerts() => _caCerts;
 156
 1157    internal Ice.Communicator communicator() => _communicator;
 158
 1159    internal int securityTraceLevel() => _securityTraceLevel;
 160
 1161    internal string securityTraceCategory() => _securityTraceCategory;
 162
 1163    internal X509Certificate2Collection certs() => _certs;
 164
 165    internal void traceStream(SslStream stream, string connInfo)
 166    {
 0167        var s = new StringBuilder();
 0168        s.Append("SSL connection summary");
 0169        if (connInfo.Length > 0)
 170        {
 0171            s.Append('\n');
 0172            s.Append(connInfo);
 173        }
 0174        s.Append("\nauthenticated = " + (stream.IsAuthenticated ? "yes" : "no"));
 0175        s.Append("\nencrypted = " + (stream.IsEncrypted ? "yes" : "no"));
 0176        s.Append("\nsigned = " + (stream.IsSigned ? "yes" : "no"));
 0177        s.Append("\nmutually authenticated = " + (stream.IsMutuallyAuthenticated ? "yes" : "no"));
 0178        s.Append("\ncipher = " + stream.NegotiatedCipherSuite);
 0179        s.Append("\nprotocol = " + stream.SslProtocol);
 0180        _logger.trace(_securityTraceCategory, s.ToString());
 0181    }
 182
 183    internal void verifyPeer(ConnectionInfo info, string description)
 184    {
 1185        if (!_trustManager.verify(info, description))
 186        {
 1187            string msg = (info.incoming ? "incoming" : "outgoing") + " connection rejected by trust manager\n" +
 1188                description;
 1189            if (_securityTraceLevel >= 1)
 190            {
 0191                _logger.trace(_securityTraceCategory, msg);
 192            }
 193
 1194            throw new SecurityException($"IceSSL: {msg}");
 195        }
 1196    }
 197
 198    internal SslClientAuthenticationOptions createClientAuthenticationOptions(
 199        RemoteCertificateValidationCallback remoteCertificateValidationCallback,
 200        string host)
 201    {
 1202        var authenticationOptions = new SslClientAuthenticationOptions
 1203        {
 1204            ClientCertificates = _certs,
 1205            LocalCertificateSelectionCallback = (sender, targetHost, certs, remoteCertificate, acceptableIssuers) =>
 1206            {
 1207                if (certs == null || certs.Count == 0)
 1208                {
 1209                    return null;
 1210                }
 1211                else if (certs.Count == 1)
 1212                {
 1213                    return certs[0];
 1214                }
 1215
 1216                // Use the first certificate that match the acceptable issuers.
 0217                if (acceptableIssuers != null && acceptableIssuers.Length > 0)
 1218                {
 0219                    foreach (X509Certificate certificate in certs)
 1220                    {
 0221                        if (Array.IndexOf(acceptableIssuers, certificate.Issuer) != -1)
 1222                        {
 0223                            return certificate;
 1224                        }
 1225                    }
 1226                }
 0227                return certs[0];
 0228            },
 1229            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1230            TargetHost = host,
 1231        };
 232
 1233        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1234        if (_caCerts is null)
 235        {
 1236            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 237        }
 238        else
 239        {
 1240            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1241            foreach (X509Certificate certificate in _caCerts)
 242            {
 1243                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 244            }
 245        }
 246
 1247        if (!_checkCertName)
 248        {
 1249            authenticationOptions.CertificateChainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreInvalidName;
 250        }
 251
 1252        if (_checkCRL == 1)
 253        {
 0254            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0255                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 256        }
 1257        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1258            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1259        return authenticationOptions;
 260    }
 261
 262    internal SslServerAuthenticationOptions createServerAuthenticationOptions(
 263        RemoteCertificateValidationCallback remoteCertificateValidationCallback)
 264    {
 265        // Get the certificate collection and select the first one.
 1266        X509Certificate2 cert = null;
 1267        if (_certs.Count > 0)
 268        {
 1269            cert = _certs[0];
 270        }
 271
 1272        var authenticationOptions = new SslServerAuthenticationOptions
 1273        {
 1274            ServerCertificate = cert,
 1275            ClientCertificateRequired = _verifyPeer > 0,
 1276            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1277            CertificateRevocationCheckMode = X509RevocationMode.NoCheck
 1278        };
 279
 1280        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1281        if (_caCerts is null)
 282        {
 0283            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 284        }
 285        else
 286        {
 1287            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1288            foreach (X509Certificate certificate in _caCerts)
 289            {
 1290                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 291            }
 292        }
 1293        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1294            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1295        if (_checkCRL == 1)
 296        {
 0297            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0298                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 299        }
 1300        return authenticationOptions;
 301    }
 302
 303    private static bool isAbsolutePath(string path)
 304    {
 305        // Skip whitespace
 1306        path = path.Trim();
 307
 1308        if (AssemblyUtil.isWindows)
 309        {
 310            // We need at least 3 non-whitespace characters to have an absolute path
 0311            if (path.Length < 3)
 312            {
 0313                return false;
 314            }
 315
 316            // Check for X:\ path ('\' may have been converted to '/')
 0317            if ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'))
 318            {
 0319                return path[1] == ':' && (path[2] == '\\' || path[2] == '/');
 320            }
 321        }
 322        // Check for UNC path
 1323        return (path[0] == '\\' && path[1] == '\\') || path[0] == '/';
 324    }
 325
 326    private static X509Certificate2Collection findCertificates(
 327        string prop,
 328        StoreLocation storeLocation,
 329        string name,
 330        string value)
 331    {
 332        // Open the X509 certificate store.
 333        X509Store store;
 334        try
 335        {
 336            try
 337            {
 1338                store = new X509Store(Enum.Parse<StoreName>(name, true), storeLocation);
 1339            }
 0340            catch (ArgumentException)
 341            {
 0342                store = new X509Store(name, storeLocation);
 0343            }
 1344            store.Open(OpenFlags.ReadOnly);
 1345        }
 0346        catch (Exception ex)
 347        {
 0348            throw new Ice.InitializationException($"IceSSL: failure while opening store specified by {prop}", ex);
 349        }
 350
 351        // Start with all of the certificates in the collection and filter as necessary.
 352        //
 353        // - If the value is "*", return all certificates.
 354        // - Otherwise, search using key:value pairs. The following keys are supported:
 355        //
 356        //   Issuer
 357        //   IssuerDN
 358        //   Serial
 359        //   Subject
 360        //   SubjectDN
 361        //   SubjectKeyId
 362        //   Thumbprint
 363        //
 364        //   A value must be enclosed in single or double quotes if it contains whitespace.
 1365        X509Certificate2Collection result = [.. store.Certificates];
 366        try
 367        {
 1368            if (value != "*")
 369            {
 1370                if (!value.Contains(':', StringComparison.Ordinal))
 371                {
 1372                    throw new Ice.InitializationException($"IceSSL: no key in `{value}'");
 373                }
 1374                int start = 0;
 375                int pos;
 1376                while ((pos = value.IndexOf(':', start)) != -1)
 377                {
 378                    // Parse the X509FindType.
 1379                    string field = value[start..pos].Trim().ToUpperInvariant();
 380                    X509FindType findType;
 1381                    if (field == "SUBJECT")
 382                    {
 1383                        findType = X509FindType.FindBySubjectName;
 384                    }
 1385                    else if (field == "SUBJECTDN")
 386                    {
 1387                        findType = X509FindType.FindBySubjectDistinguishedName;
 388                    }
 1389                    else if (field == "ISSUER")
 390                    {
 1391                        findType = X509FindType.FindByIssuerName;
 392                    }
 1393                    else if (field == "ISSUERDN")
 394                    {
 1395                        findType = X509FindType.FindByIssuerDistinguishedName;
 396                    }
 1397                    else if (field == "THUMBPRINT")
 398                    {
 1399                        findType = X509FindType.FindByThumbprint;
 400                    }
 1401                    else if (field == "SUBJECTKEYID")
 402                    {
 1403                        findType = X509FindType.FindBySubjectKeyIdentifier;
 404                    }
 1405                    else if (field == "SERIAL")
 406                    {
 1407                        findType = X509FindType.FindBySerialNumber;
 408                    }
 409                    else
 410                    {
 1411                        throw new Ice.InitializationException($"IceSSL: unknown key in `{value}'");
 412                    }
 413
 414                    // Parse the argument.
 1415                    start = pos + 1;
 1416                    while (start < value.Length && (value[start] == ' ' || value[start] == '\t'))
 417                    {
 0418                        ++start;
 419                    }
 420
 1421                    if (start == value.Length)
 422                    {
 0423                        throw new Ice.InitializationException($"IceSSL: missing argument in `{value}'");
 424                    }
 425
 426                    string arg;
 1427                    if (value[start] == '"' || value[start] == '\'')
 428                    {
 1429                        int end = start;
 1430                        ++end;
 1431                        while (end < value.Length)
 432                        {
 1433                            if (value[end] == value[start] && value[end - 1] != '\\')
 434                            {
 435                                break;
 436                            }
 1437                            ++end;
 438                        }
 1439                        if (end == value.Length || value[end] != value[start])
 440                        {
 0441                            throw new Ice.InitializationException($"IceSSL: unmatched quote in `{value}'");
 442                        }
 1443                        ++start;
 1444                        arg = value[start..end];
 1445                        start = end + 1;
 446                    }
 447                    else
 448                    {
 1449                        char[] ws = [' ', '\t'];
 1450                        int end = value.IndexOfAny(ws, start);
 1451                        if (end == -1)
 452                        {
 1453                            arg = value[start..];
 1454                            start = value.Length;
 455                        }
 456                        else
 457                        {
 1458                            arg = value[start..end];
 1459                            start = end + 1;
 460                        }
 461                    }
 462
 463                    // Execute the query.
 464                    //
 465                    // TODO: allow user to specify a value for validOnly?
 1466                    bool validOnly = false;
 1467                    if (findType == X509FindType.FindBySubjectDistinguishedName ||
 1468                        findType == X509FindType.FindByIssuerDistinguishedName)
 469                    {
 1470                        X500DistinguishedNameFlags[] flags = [
 1471                            X500DistinguishedNameFlags.None,
 1472                            X500DistinguishedNameFlags.Reversed,
 1473                        ];
 1474                        var dn = new X500DistinguishedName(arg);
 1475                        X509Certificate2Collection r = result;
 1476                        for (int i = 0; i < flags.Length; ++i)
 477                        {
 1478                            r = result.Find(findType, dn.Decode(flags[i]), validOnly);
 1479                            if (r.Count > 0)
 480                            {
 481                                break;
 482                            }
 483                        }
 1484                        result = r;
 485                    }
 486                    else
 487                    {
 1488                        result = result.Find(findType, arg, validOnly);
 489                    }
 490                }
 491            }
 1492        }
 493        finally
 494        {
 1495            store.Close();
 1496        }
 497
 1498        return result;
 499    }
 500
 501    private static SecureString createSecureString(string s)
 502    {
 1503        var result = new SecureString();
 1504        foreach (char ch in s)
 505        {
 1506            result.AppendChar(ch);
 507        }
 1508        return result;
 509    }
 510
 511    private bool checkPath(ref string path)
 512    {
 1513        if (File.Exists(path))
 514        {
 0515            return true;
 516        }
 517
 1518        if (_defaultDir.Length > 0 && !isAbsolutePath(path))
 519        {
 1520            string s = _defaultDir + Path.DirectorySeparatorChar + path;
 1521            if (File.Exists(s))
 522            {
 1523                path = s;
 1524                return true;
 525            }
 526        }
 0527        return false;
 528    }
 529
 530    private readonly Ice.Communicator _communicator;
 531    private readonly Ice.Logger _logger;
 532    private readonly int _securityTraceLevel;
 533    private readonly string _securityTraceCategory;
 534    private string _defaultDir;
 535    private bool _checkCertName;
 536    private int _verifyPeer;
 537    private int _checkCRL;
 538    private X509Certificate2Collection _certs;
 539    private bool _useMachineContext;
 540    private X509Certificate2Collection _caCerts;
 541    private readonly TrustManager _trustManager;
 542}