< Summary

Information
Class: Ice.SSL.SSLEngine
Assembly: Ice
File(s): /_/csharp/src/Ice/SSL/SSLEngine.cs
Tag: 99_23991109993
Line coverage
75%
Covered lines: 188
Uncovered lines: 62
Coverable lines: 250
Total lines: 549
Line coverage: 75.2%
Branch coverage
63%
Covered branches: 106
Total branches: 168
Branch coverage: 63%
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()73.33%60.053067.8%
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(...)75%12.11291.18%
createServerAuthenticationOptions(...)70%10.291085.71%
isAbsolutePath(...)20.83%122.792444.44%
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");
 132        if (_verifyPeer < 0 || _verifyPeer > 2)
 33        {
 034            throw new Ice.InitializationException("SSL transport: invalid value for IceSSL.VerifyPeer");
 35        }
 36
 37        // CheckCRL determines whether the certificate revocation list is checked, and how strictly.
 138        _checkCRL = properties.getIcePropertyAsInt("IceSSL.CheckCRL");
 39
 140        string certStoreLocation = properties.getIceProperty("IceSSL.CertStoreLocation");
 41        StoreLocation storeLocation;
 142        if (certStoreLocation == "CurrentUser")
 43        {
 144            storeLocation = StoreLocation.CurrentUser;
 45        }
 046        else if (certStoreLocation == "LocalMachine")
 47        {
 048            storeLocation = StoreLocation.LocalMachine;
 49        }
 50        else
 51        {
 052            _logger.warning(
 053                "Invalid IceSSL.CertStoreLocation value `" + certStoreLocation + "' adjusted to `CurrentUser'");
 054            storeLocation = StoreLocation.CurrentUser;
 55        }
 156        _useMachineContext = certStoreLocation == "LocalMachine";
 57
 58        // CheckCertName determines whether we compare the name in a peer's certificate against its hostname.
 159        _checkCertName = properties.getIcePropertyAsInt("IceSSL.CheckCertName") > 0;
 60
 61        Debug.Assert(_certs == null);
 62        // If IceSSL.CertFile is defined, load a certificate from a file and add it to the collection.
 163        _certs = [];
 164        string certFile = properties.getIceProperty("IceSSL.CertFile");
 165        string passwordStr = properties.getIceProperty("IceSSL.Password");
 166        string findCert = properties.getIceProperty("IceSSL.FindCert");
 67
 168        if (certFile.Length > 0)
 69        {
 170            if (!checkPath(ref certFile))
 71            {
 072                throw new Ice.InitializationException($"IceSSL: certificate file not found: {certFile}");
 73            }
 74
 75            try
 76            {
 77                X509Certificate2 cert;
 78                X509KeyStorageFlags importFlags;
 179                if (_useMachineContext)
 80                {
 081                    importFlags = X509KeyStorageFlags.MachineKeySet;
 82                }
 83                else
 84                {
 185                    importFlags = X509KeyStorageFlags.UserKeySet;
 86                }
 87
 188                if (passwordStr.Length > 0)
 89                {
 190                    using SecureString password = createSecureString(passwordStr);
 191                    cert = new X509Certificate2(certFile, password, importFlags);
 92                }
 93                else
 94                {
 195                    cert = new X509Certificate2(certFile, (string)null, importFlags);
 96                }
 197                _certs.Add(cert);
 198            }
 099            catch (CryptographicException ex)
 100            {
 0101                throw new Ice.InitializationException(
 0102                    $"IceSSL: error while attempting to load certificate from {certFile}",
 0103                    ex);
 104            }
 105        }
 1106        else if (findCert.Length > 0)
 107        {
 1108            string certStore = properties.getIceProperty("IceSSL.CertStore");
 1109            _certs.AddRange(findCertificates("IceSSL.FindCert", storeLocation, certStore, findCert));
 1110            if (_certs.Count == 0)
 111            {
 1112                throw new Ice.InitializationException("IceSSL: no certificates found");
 113            }
 114        }
 115
 116        Debug.Assert(_caCerts == null);
 1117        string certAuthFile = properties.getIceProperty("IceSSL.CAs");
 1118        if (certAuthFile.Length > 0 || properties.getIcePropertyAsInt("IceSSL.UsePlatformCAs") <= 0)
 119        {
 1120            _caCerts = [];
 121        }
 122
 1123        if (certAuthFile.Length > 0)
 124        {
 1125            if (!checkPath(ref certAuthFile))
 126            {
 0127                throw new Ice.InitializationException($"IceSSL: CA certificate file not found: {certAuthFile}");
 128            }
 129            try
 130            {
 131                try
 132                {
 133                    // First try to import as a PEM file, which supports importing multiple certificates from a PEM
 134                    // encoded file
 1135                    _caCerts.ImportFromPemFile(certAuthFile);
 1136                }
 0137                catch (CryptographicException)
 138                {
 139                    // Expected if the file is not in PEM format.
 0140                }
 141
 1142                if (_caCerts.Count == 0)
 143                {
 144                    // Fallback to Import which handles DER/PFX.
 1145                    _caCerts.Import(certAuthFile);
 146                }
 1147            }
 0148            catch (Exception ex)
 149            {
 0150                throw new Ice.InitializationException(
 0151                    $"IceSSL: error while attempting to load CA certificate from {certAuthFile}",
 0152                    ex);
 153            }
 154        }
 1155    }
 156
 0157    internal bool useMachineContext() => _useMachineContext;
 158
 0159    internal X509Certificate2Collection caCerts() => _caCerts;
 160
 1161    internal Ice.Communicator communicator() => _communicator;
 162
 1163    internal int securityTraceLevel() => _securityTraceLevel;
 164
 1165    internal string securityTraceCategory() => _securityTraceCategory;
 166
 1167    internal X509Certificate2Collection certs() => _certs;
 168
 169    internal void traceStream(SslStream stream, string connInfo)
 170    {
 0171        var s = new StringBuilder();
 0172        s.Append("SSL connection summary");
 0173        if (connInfo.Length > 0)
 174        {
 0175            s.Append('\n');
 0176            s.Append(connInfo);
 177        }
 0178        s.Append("\nauthenticated = " + (stream.IsAuthenticated ? "yes" : "no"));
 0179        s.Append("\nencrypted = " + (stream.IsEncrypted ? "yes" : "no"));
 0180        s.Append("\nsigned = " + (stream.IsSigned ? "yes" : "no"));
 0181        s.Append("\nmutually authenticated = " + (stream.IsMutuallyAuthenticated ? "yes" : "no"));
 0182        s.Append("\ncipher = " + stream.NegotiatedCipherSuite);
 0183        s.Append("\nprotocol = " + stream.SslProtocol);
 0184        _logger.trace(_securityTraceCategory, s.ToString());
 0185    }
 186
 187    internal void verifyPeer(ConnectionInfo info, string description)
 188    {
 1189        if (!_trustManager.verify(info, description))
 190        {
 1191            string msg = (info.incoming ? "incoming" : "outgoing") + " connection rejected by trust manager\n" +
 1192                description;
 1193            if (_securityTraceLevel >= 1)
 194            {
 0195                _logger.trace(_securityTraceCategory, msg);
 196            }
 197
 1198            throw new SecurityException($"IceSSL: {msg}");
 199        }
 1200    }
 201
 202    internal SslClientAuthenticationOptions createClientAuthenticationOptions(
 203        RemoteCertificateValidationCallback remoteCertificateValidationCallback,
 204        string host)
 205    {
 1206        var authenticationOptions = new SslClientAuthenticationOptions
 1207        {
 1208            ClientCertificates = _certs,
 1209            LocalCertificateSelectionCallback = (sender, targetHost, certs, remoteCertificate, acceptableIssuers) =>
 1210            {
 1211                if (certs == null || certs.Count == 0)
 1212                {
 1213                    return null;
 1214                }
 1215                else if (certs.Count == 1)
 1216                {
 1217                    return certs[0];
 1218                }
 1219
 1220                // Use the first certificate that match the acceptable issuers.
 0221                if (acceptableIssuers != null && acceptableIssuers.Length > 0)
 1222                {
 0223                    foreach (X509Certificate certificate in certs)
 1224                    {
 0225                        if (Array.IndexOf(acceptableIssuers, certificate.Issuer) != -1)
 1226                        {
 0227                            return certificate;
 1228                        }
 1229                    }
 1230                }
 0231                return certs[0];
 0232            },
 1233            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1234            TargetHost = host,
 1235        };
 236
 1237        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1238        if (_caCerts is null)
 239        {
 0240            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 241        }
 242        else
 243        {
 1244            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1245            foreach (X509Certificate certificate in _caCerts)
 246            {
 1247                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 248            }
 249        }
 250
 1251        if (!_checkCertName)
 252        {
 1253            authenticationOptions.CertificateChainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreInvalidName;
 254        }
 255
 1256        if (_checkCRL == 1)
 257        {
 0258            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0259                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 260        }
 1261        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1262            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1263        return authenticationOptions;
 264    }
 265
 266    internal SslServerAuthenticationOptions createServerAuthenticationOptions(
 267        RemoteCertificateValidationCallback remoteCertificateValidationCallback)
 268    {
 269        // Get the certificate collection and select the first one.
 1270        X509Certificate2 cert = null;
 1271        if (_certs.Count > 0)
 272        {
 1273            cert = _certs[0];
 274        }
 275
 1276        var authenticationOptions = new SslServerAuthenticationOptions
 1277        {
 1278            ServerCertificate = cert,
 1279            ClientCertificateRequired = _verifyPeer > 0,
 1280            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1281        };
 282
 1283        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1284        if (_caCerts is null)
 285        {
 0286            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 287        }
 288        else
 289        {
 1290            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1291            foreach (X509Certificate certificate in _caCerts)
 292            {
 1293                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 294            }
 295        }
 1296        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1297            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1298        if (_checkCRL == 1)
 299        {
 0300            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0301                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 302        }
 1303        return authenticationOptions;
 304    }
 305
 306    private static bool isAbsolutePath(string path)
 307    {
 308        // Skip whitespace
 1309        path = path.Trim();
 1310        if (path.Length == 0)
 311        {
 0312            return false;
 313        }
 314
 1315        if (AssemblyUtil.isWindows)
 316        {
 317            // We need at least 3 non-whitespace characters to have an absolute path
 0318            if (path.Length < 3)
 319            {
 0320                return false;
 321            }
 322
 323            // Check for X:\ path ('\' may have been converted to '/')
 0324            if ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'))
 325            {
 0326                return path[1] == ':' && (path[2] == '\\' || path[2] == '/');
 327            }
 328        }
 329        // Check for UNC path or Unix absolute path
 1330        return (path.Length >= 2 && path[0] == '\\' && path[1] == '\\') || path[0] == '/';
 331    }
 332
 333    private static X509Certificate2Collection findCertificates(
 334        string prop,
 335        StoreLocation storeLocation,
 336        string name,
 337        string value)
 338    {
 339        // Open the X509 certificate store.
 340        X509Store store;
 341        try
 342        {
 343            try
 344            {
 1345                store = new X509Store(Enum.Parse<StoreName>(name, true), storeLocation);
 1346            }
 0347            catch (ArgumentException)
 348            {
 0349                store = new X509Store(name, storeLocation);
 0350            }
 1351            store.Open(OpenFlags.ReadOnly);
 1352        }
 0353        catch (Exception ex)
 354        {
 0355            throw new Ice.InitializationException($"IceSSL: failure while opening store specified by {prop}", ex);
 356        }
 357
 358        // Start with all of the certificates in the collection and filter as necessary.
 359        //
 360        // - If the value is "*", return all certificates.
 361        // - Otherwise, search using key:value pairs. The following keys are supported:
 362        //
 363        //   Issuer
 364        //   IssuerDN
 365        //   Serial
 366        //   Subject
 367        //   SubjectDN
 368        //   SubjectKeyId
 369        //   Thumbprint
 370        //
 371        //   A value must be enclosed in single or double quotes if it contains whitespace.
 1372        X509Certificate2Collection result = [.. store.Certificates];
 373        try
 374        {
 1375            if (value != "*")
 376            {
 1377                if (!value.Contains(':', StringComparison.Ordinal))
 378                {
 1379                    throw new Ice.InitializationException($"IceSSL: no key in `{value}'");
 380                }
 1381                int start = 0;
 382                int pos;
 1383                while ((pos = value.IndexOf(':', start)) != -1)
 384                {
 385                    // Parse the X509FindType.
 1386                    string field = value[start..pos].Trim().ToUpperInvariant();
 387                    X509FindType findType;
 1388                    if (field == "SUBJECT")
 389                    {
 1390                        findType = X509FindType.FindBySubjectName;
 391                    }
 1392                    else if (field == "SUBJECTDN")
 393                    {
 1394                        findType = X509FindType.FindBySubjectDistinguishedName;
 395                    }
 1396                    else if (field == "ISSUER")
 397                    {
 1398                        findType = X509FindType.FindByIssuerName;
 399                    }
 1400                    else if (field == "ISSUERDN")
 401                    {
 1402                        findType = X509FindType.FindByIssuerDistinguishedName;
 403                    }
 1404                    else if (field == "THUMBPRINT")
 405                    {
 1406                        findType = X509FindType.FindByThumbprint;
 407                    }
 1408                    else if (field == "SUBJECTKEYID")
 409                    {
 1410                        findType = X509FindType.FindBySubjectKeyIdentifier;
 411                    }
 1412                    else if (field == "SERIAL")
 413                    {
 1414                        findType = X509FindType.FindBySerialNumber;
 415                    }
 416                    else
 417                    {
 1418                        throw new Ice.InitializationException($"IceSSL: unknown key in `{value}'");
 419                    }
 420
 421                    // Parse the argument.
 1422                    start = pos + 1;
 1423                    while (start < value.Length && (value[start] == ' ' || value[start] == '\t'))
 424                    {
 0425                        ++start;
 426                    }
 427
 1428                    if (start == value.Length)
 429                    {
 0430                        throw new Ice.InitializationException($"IceSSL: missing argument in `{value}'");
 431                    }
 432
 433                    string arg;
 1434                    if (value[start] == '"' || value[start] == '\'')
 435                    {
 1436                        int end = start;
 1437                        ++end;
 1438                        while (end < value.Length)
 439                        {
 1440                            if (value[end] == value[start] && value[end - 1] != '\\')
 441                            {
 442                                break;
 443                            }
 1444                            ++end;
 445                        }
 1446                        if (end == value.Length || value[end] != value[start])
 447                        {
 0448                            throw new Ice.InitializationException($"IceSSL: unmatched quote in `{value}'");
 449                        }
 1450                        ++start;
 1451                        arg = value[start..end];
 1452                        start = end + 1;
 453                    }
 454                    else
 455                    {
 1456                        char[] ws = [' ', '\t'];
 1457                        int end = value.IndexOfAny(ws, start);
 1458                        if (end == -1)
 459                        {
 1460                            arg = value[start..];
 1461                            start = value.Length;
 462                        }
 463                        else
 464                        {
 1465                            arg = value[start..end];
 1466                            start = end + 1;
 467                        }
 468                    }
 469
 470                    // Execute the query.
 471                    //
 472                    // TODO: allow user to specify a value for validOnly?
 1473                    bool validOnly = false;
 1474                    if (findType == X509FindType.FindBySubjectDistinguishedName ||
 1475                        findType == X509FindType.FindByIssuerDistinguishedName)
 476                    {
 1477                        X500DistinguishedNameFlags[] flags = [
 1478                            X500DistinguishedNameFlags.None,
 1479                            X500DistinguishedNameFlags.Reversed,
 1480                        ];
 1481                        var dn = new X500DistinguishedName(arg);
 1482                        X509Certificate2Collection r = result;
 1483                        for (int i = 0; i < flags.Length; ++i)
 484                        {
 1485                            r = result.Find(findType, dn.Decode(flags[i]), validOnly);
 1486                            if (r.Count > 0)
 487                            {
 488                                break;
 489                            }
 490                        }
 1491                        result = r;
 492                    }
 493                    else
 494                    {
 1495                        result = result.Find(findType, arg, validOnly);
 496                    }
 497                }
 498            }
 1499        }
 500        finally
 501        {
 1502            store.Close();
 1503        }
 504
 1505        return result;
 506    }
 507
 508    private static SecureString createSecureString(string s)
 509    {
 1510        var result = new SecureString();
 1511        foreach (char ch in s)
 512        {
 1513            result.AppendChar(ch);
 514        }
 1515        return result;
 516    }
 517
 518    private bool checkPath(ref string path)
 519    {
 1520        if (File.Exists(path))
 521        {
 0522            return true;
 523        }
 524
 1525        if (_defaultDir.Length > 0 && !isAbsolutePath(path))
 526        {
 1527            string s = _defaultDir + Path.DirectorySeparatorChar + path;
 1528            if (File.Exists(s))
 529            {
 1530                path = s;
 1531                return true;
 532            }
 533        }
 0534        return false;
 535    }
 536
 537    private readonly Ice.Communicator _communicator;
 538    private readonly Ice.Logger _logger;
 539    private readonly int _securityTraceLevel;
 540    private readonly string _securityTraceCategory;
 541    private string _defaultDir;
 542    private bool _checkCertName;
 543    private int _verifyPeer;
 544    private int _checkCRL;
 545    private X509Certificate2Collection _certs;
 546    private bool _useMachineContext;
 547    private X509Certificate2Collection _caCerts;
 548    private readonly TrustManager _trustManager;
 549}