< Summary

Information
Class: Ice.SSL.SSLEngine
Assembly: Ice
File(s): /_/csharp/src/Ice/SSL/SSLEngine.cs
Tag: 125_37167941578
Line coverage
76%
Covered lines: 197
Uncovered lines: 60
Coverable lines: 257
Total lines: 572
Line coverage: 76.6%
Branch coverage
64%
Covered branches: 114
Total branches: 176
Branch coverage: 64.7%
Method coverage
93%
Covered methods: 14
Fully covered methods: 7
Total methods: 15
Method coverage: 93.3%
Full method coverage: 46.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Dispose()100%88100%
initialize()73.33%603067.8%
communicator()100%11100%
securityTraceLevel()100%11100%
securityTraceCategory()100%11100%
certs()100%11100%
traceStream(...)0%110100%
verifyPeer(...)83.33%6685.71%
createClientAuthenticationOptions(...)75%121291.18%
createServerAuthenticationOptions(...)70%101085.71%
isAbsolutePath(...)20.83%1232444.44%
findCertificates(...)90.38%565288.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 : IDisposable
 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    public void Dispose()
 25    {
 26        // Release the unmanaged key handles that back each X509Certificate2. The certificate
 27        // collection itself isn't IDisposable, so we have to walk its contents.
 128        if (_certs is not null)
 29        {
 130            foreach (X509Certificate2 cert in _certs)
 31            {
 132                cert.Dispose();
 33            }
 134            _certs = null;
 35        }
 36
 137        if (_caCerts is not null)
 38        {
 139            foreach (X509Certificate2 cert in _caCerts)
 40            {
 141                cert.Dispose();
 42            }
 143            _caCerts = null;
 44        }
 145    }
 46
 47    internal void initialize()
 48    {
 149        Ice.Properties properties = communicator().getProperties();
 50
 51        // Check for a default directory. We look in this directory for files mentioned in the configuration.
 152        _defaultDir = properties.getIceProperty("IceSSL.DefaultDir");
 53
 154        _verifyPeer = properties.getIcePropertyAsInt("IceSSL.VerifyPeer");
 155        if (_verifyPeer < 0 || _verifyPeer > 2)
 56        {
 057            throw new Ice.InitializationException("SSL transport: invalid value for IceSSL.VerifyPeer");
 58        }
 59
 60        // CheckCRL determines whether the certificate revocation list is checked, and how strictly.
 161        _checkCRL = properties.getIcePropertyAsInt("IceSSL.CheckCRL");
 62
 163        string certStoreLocation = properties.getIceProperty("IceSSL.CertStoreLocation");
 64        StoreLocation storeLocation;
 165        if (certStoreLocation == "CurrentUser")
 66        {
 167            storeLocation = StoreLocation.CurrentUser;
 68        }
 069        else if (certStoreLocation == "LocalMachine")
 70        {
 071            storeLocation = StoreLocation.LocalMachine;
 72        }
 73        else
 74        {
 075            _logger.warning(
 076                "Invalid IceSSL.CertStoreLocation value `" + certStoreLocation + "' adjusted to `CurrentUser'");
 077            storeLocation = StoreLocation.CurrentUser;
 78        }
 179        _useMachineContext = certStoreLocation == "LocalMachine";
 80
 81        // CheckCertName determines whether we compare the name in a peer's certificate against its hostname.
 182        _checkCertName = properties.getIcePropertyAsInt("IceSSL.CheckCertName") > 0;
 83
 84        Debug.Assert(_certs == null);
 85        // If IceSSL.CertFile is defined, load a certificate from a file and add it to the collection.
 186        _certs = [];
 187        string certFile = properties.getIceProperty("IceSSL.CertFile");
 188        string passwordStr = properties.getIceProperty("IceSSL.Password");
 189        string findCert = properties.getIceProperty("IceSSL.FindCert");
 90
 191        if (certFile.Length > 0)
 92        {
 193            if (!checkPath(ref certFile))
 94            {
 095                throw new Ice.InitializationException($"IceSSL: certificate file not found: {certFile}");
 96            }
 97
 98            try
 99            {
 100                X509Certificate2 cert;
 101                X509KeyStorageFlags importFlags;
 1102                if (_useMachineContext)
 103                {
 0104                    importFlags = X509KeyStorageFlags.MachineKeySet;
 105                }
 106                else
 107                {
 1108                    importFlags = X509KeyStorageFlags.UserKeySet;
 109                }
 110
 1111                if (passwordStr.Length > 0)
 112                {
 1113                    using SecureString password = createSecureString(passwordStr);
 1114                    cert = new X509Certificate2(certFile, password, importFlags);
 115                }
 116                else
 117                {
 1118                    cert = new X509Certificate2(certFile, (string)null, importFlags);
 119                }
 1120                _certs.Add(cert);
 1121            }
 0122            catch (CryptographicException ex)
 123            {
 0124                throw new Ice.InitializationException(
 0125                    $"IceSSL: error while attempting to load certificate from {certFile}",
 0126                    ex);
 127            }
 128        }
 1129        else if (findCert.Length > 0)
 130        {
 1131            string certStore = properties.getIceProperty("IceSSL.CertStore");
 1132            _certs.AddRange(findCertificates("IceSSL.FindCert", storeLocation, certStore, findCert));
 1133            if (_certs.Count == 0)
 134            {
 1135                throw new Ice.InitializationException("IceSSL: no certificates found");
 136            }
 137        }
 138
 139        Debug.Assert(_caCerts == null);
 1140        string certAuthFile = properties.getIceProperty("IceSSL.CAs");
 1141        if (certAuthFile.Length > 0 || properties.getIcePropertyAsInt("IceSSL.UsePlatformCAs") <= 0)
 142        {
 1143            _caCerts = [];
 144        }
 145
 1146        if (certAuthFile.Length > 0)
 147        {
 1148            if (!checkPath(ref certAuthFile))
 149            {
 0150                throw new Ice.InitializationException($"IceSSL: CA certificate file not found: {certAuthFile}");
 151            }
 152            try
 153            {
 154                try
 155                {
 156                    // First try to import as a PEM file, which supports importing multiple certificates from a PEM
 157                    // encoded file
 1158                    _caCerts.ImportFromPemFile(certAuthFile);
 1159                }
 0160                catch (CryptographicException)
 161                {
 162                    // Expected if the file is not in PEM format.
 0163                }
 164
 1165                if (_caCerts.Count == 0)
 166                {
 167                    // Fallback to Import which handles DER/PFX.
 1168                    _caCerts.Import(certAuthFile);
 169                }
 1170            }
 0171            catch (Exception ex)
 172            {
 0173                throw new Ice.InitializationException(
 0174                    $"IceSSL: error while attempting to load CA certificate from {certAuthFile}",
 0175                    ex);
 176            }
 177        }
 1178    }
 179
 1180    internal Ice.Communicator communicator() => _communicator;
 181
 1182    internal int securityTraceLevel() => _securityTraceLevel;
 183
 1184    internal string securityTraceCategory() => _securityTraceCategory;
 185
 1186    internal X509Certificate2Collection certs() => _certs;
 187
 188    internal void traceStream(SslStream stream, string connInfo)
 189    {
 0190        var s = new StringBuilder();
 0191        s.Append("SSL connection summary");
 0192        if (connInfo.Length > 0)
 193        {
 0194            s.Append('\n');
 0195            s.Append(connInfo);
 196        }
 0197        s.Append("\nauthenticated = " + (stream.IsAuthenticated ? "yes" : "no"));
 0198        s.Append("\nencrypted = " + (stream.IsEncrypted ? "yes" : "no"));
 0199        s.Append("\nsigned = " + (stream.IsSigned ? "yes" : "no"));
 0200        s.Append("\nmutually authenticated = " + (stream.IsMutuallyAuthenticated ? "yes" : "no"));
 0201        s.Append("\ncipher = " + stream.NegotiatedCipherSuite);
 0202        s.Append("\nprotocol = " + stream.SslProtocol);
 0203        _logger.trace(_securityTraceCategory, s.ToString());
 0204    }
 205
 206    internal void verifyPeer(ConnectionInfo info, string description)
 207    {
 1208        if (!_trustManager.verify(info, description))
 209        {
 1210            string msg = (info.incoming ? "incoming" : "outgoing") + " connection rejected by trust manager\n" +
 1211                description;
 1212            if (_securityTraceLevel >= 1)
 213            {
 0214                _logger.trace(_securityTraceCategory, msg);
 215            }
 216
 1217            throw new SecurityException($"IceSSL: {msg}");
 218        }
 1219    }
 220
 221    internal SslClientAuthenticationOptions createClientAuthenticationOptions(
 222        RemoteCertificateValidationCallback remoteCertificateValidationCallback,
 223        string host)
 224    {
 1225        var authenticationOptions = new SslClientAuthenticationOptions
 1226        {
 1227            ClientCertificates = _certs,
 1228            LocalCertificateSelectionCallback = (sender, targetHost, certs, remoteCertificate, acceptableIssuers) =>
 1229            {
 1230                if (certs == null || certs.Count == 0)
 1231                {
 1232                    return null;
 1233                }
 1234                else if (certs.Count == 1)
 1235                {
 1236                    return certs[0];
 1237                }
 1238
 1239                // Use the first certificate that match the acceptable issuers.
 0240                if (acceptableIssuers != null && acceptableIssuers.Length > 0)
 1241                {
 0242                    foreach (X509Certificate certificate in certs)
 1243                    {
 0244                        if (Array.IndexOf(acceptableIssuers, certificate.Issuer) != -1)
 1245                        {
 0246                            return certificate;
 1247                        }
 1248                    }
 1249                }
 0250                return certs[0];
 0251            },
 1252            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1253            TargetHost = host,
 1254        };
 255
 1256        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1257        if (_caCerts is null)
 258        {
 0259            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 260        }
 261        else
 262        {
 1263            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1264            foreach (X509Certificate certificate in _caCerts)
 265            {
 1266                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 267            }
 268        }
 269
 1270        if (!_checkCertName)
 271        {
 1272            authenticationOptions.CertificateChainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreInvalidName;
 273        }
 274
 1275        if (_checkCRL == 1)
 276        {
 0277            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0278                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 279        }
 1280        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1281            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1282        return authenticationOptions;
 283    }
 284
 285    internal SslServerAuthenticationOptions createServerAuthenticationOptions(
 286        RemoteCertificateValidationCallback remoteCertificateValidationCallback)
 287    {
 288        // Get the certificate collection and select the first one.
 1289        X509Certificate2 cert = null;
 1290        if (_certs.Count > 0)
 291        {
 1292            cert = _certs[0];
 293        }
 294
 1295        var authenticationOptions = new SslServerAuthenticationOptions
 1296        {
 1297            ServerCertificate = cert,
 1298            ClientCertificateRequired = _verifyPeer > 0,
 1299            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1300        };
 301
 1302        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1303        if (_caCerts is null)
 304        {
 0305            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 306        }
 307        else
 308        {
 1309            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1310            foreach (X509Certificate certificate in _caCerts)
 311            {
 1312                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 313            }
 314        }
 1315        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1316            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1317        if (_checkCRL == 1)
 318        {
 0319            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0320                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 321        }
 1322        return authenticationOptions;
 323    }
 324
 325    private static bool isAbsolutePath(string path)
 326    {
 327        // Skip whitespace
 1328        path = path.Trim();
 1329        if (path.Length == 0)
 330        {
 0331            return false;
 332        }
 333
 1334        if (AssemblyUtil.isWindows)
 335        {
 336            // We need at least 3 non-whitespace characters to have an absolute path
 0337            if (path.Length < 3)
 338            {
 0339                return false;
 340            }
 341
 342            // Check for X:\ path ('\' may have been converted to '/')
 0343            if ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'))
 344            {
 0345                return path[1] == ':' && (path[2] == '\\' || path[2] == '/');
 346            }
 347        }
 348        // Check for UNC path or Unix absolute path
 1349        return (path.Length >= 2 && path[0] == '\\' && path[1] == '\\') || path[0] == '/';
 350    }
 351
 352    private static X509Certificate2Collection findCertificates(
 353        string prop,
 354        StoreLocation storeLocation,
 355        string name,
 356        string value)
 357    {
 358        // Open the X509 certificate store.
 359        X509Store store;
 360        try
 361        {
 362            try
 363            {
 1364                store = new X509Store(Enum.Parse<StoreName>(name, true), storeLocation);
 1365            }
 0366            catch (ArgumentException)
 367            {
 0368                store = new X509Store(name, storeLocation);
 0369            }
 1370            store.Open(OpenFlags.ReadOnly);
 1371        }
 0372        catch (Exception ex)
 373        {
 0374            throw new Ice.InitializationException($"IceSSL: failure while opening store specified by {prop}", ex);
 375        }
 376
 377        // Start with all of the certificates in the collection and filter as necessary.
 378        //
 379        // - If the value is "*", return all certificates.
 380        // - Otherwise, search using key:value pairs. The following keys are supported:
 381        //
 382        //   Issuer
 383        //   IssuerDN
 384        //   Serial
 385        //   Subject
 386        //   SubjectDN
 387        //   SubjectKeyId
 388        //   Thumbprint
 389        //
 390        //   A value must be enclosed in single or double quotes if it contains whitespace.
 1391        X509Certificate2Collection result = [.. store.Certificates];
 392        try
 393        {
 1394            if (value != "*")
 395            {
 1396                if (!value.Contains(':', StringComparison.Ordinal))
 397                {
 1398                    throw new Ice.InitializationException($"IceSSL: no key in `{value}'");
 399                }
 1400                int start = 0;
 401                int pos;
 1402                while ((pos = value.IndexOf(':', start)) != -1)
 403                {
 404                    // Parse the X509FindType.
 1405                    string field = value[start..pos].Trim().ToUpperInvariant();
 406                    X509FindType findType;
 1407                    if (field == "SUBJECT")
 408                    {
 1409                        findType = X509FindType.FindBySubjectName;
 410                    }
 1411                    else if (field == "SUBJECTDN")
 412                    {
 1413                        findType = X509FindType.FindBySubjectDistinguishedName;
 414                    }
 1415                    else if (field == "ISSUER")
 416                    {
 1417                        findType = X509FindType.FindByIssuerName;
 418                    }
 1419                    else if (field == "ISSUERDN")
 420                    {
 1421                        findType = X509FindType.FindByIssuerDistinguishedName;
 422                    }
 1423                    else if (field == "THUMBPRINT")
 424                    {
 1425                        findType = X509FindType.FindByThumbprint;
 426                    }
 1427                    else if (field == "SUBJECTKEYID")
 428                    {
 1429                        findType = X509FindType.FindBySubjectKeyIdentifier;
 430                    }
 1431                    else if (field == "SERIAL")
 432                    {
 1433                        findType = X509FindType.FindBySerialNumber;
 434                    }
 435                    else
 436                    {
 1437                        throw new Ice.InitializationException($"IceSSL: unknown key in `{value}'");
 438                    }
 439
 440                    // Parse the argument.
 1441                    start = pos + 1;
 1442                    while (start < value.Length && (value[start] == ' ' || value[start] == '\t'))
 443                    {
 0444                        ++start;
 445                    }
 446
 1447                    if (start == value.Length)
 448                    {
 0449                        throw new Ice.InitializationException($"IceSSL: missing argument in `{value}'");
 450                    }
 451
 452                    string arg;
 1453                    if (value[start] == '"' || value[start] == '\'')
 454                    {
 1455                        int end = start;
 1456                        ++end;
 1457                        while (end < value.Length)
 458                        {
 1459                            if (value[end] == value[start] && value[end - 1] != '\\')
 460                            {
 461                                break;
 462                            }
 1463                            ++end;
 464                        }
 1465                        if (end == value.Length || value[end] != value[start])
 466                        {
 0467                            throw new Ice.InitializationException($"IceSSL: unmatched quote in `{value}'");
 468                        }
 1469                        ++start;
 1470                        arg = value[start..end];
 1471                        start = end + 1;
 472                    }
 473                    else
 474                    {
 1475                        char[] ws = [' ', '\t'];
 1476                        int end = value.IndexOfAny(ws, start);
 1477                        if (end == -1)
 478                        {
 1479                            arg = value[start..];
 1480                            start = value.Length;
 481                        }
 482                        else
 483                        {
 1484                            arg = value[start..end];
 1485                            start = end + 1;
 486                        }
 487                    }
 488
 489                    // Execute the query.
 490                    //
 491                    // TODO: allow user to specify a value for validOnly?
 1492                    bool validOnly = false;
 1493                    if (findType == X509FindType.FindBySubjectDistinguishedName ||
 1494                        findType == X509FindType.FindByIssuerDistinguishedName)
 495                    {
 1496                        X500DistinguishedNameFlags[] flags = [
 1497                            X500DistinguishedNameFlags.None,
 1498                            X500DistinguishedNameFlags.Reversed,
 1499                        ];
 1500                        var dn = new X500DistinguishedName(arg);
 1501                        X509Certificate2Collection r = result;
 1502                        for (int i = 0; i < flags.Length; ++i)
 503                        {
 1504                            r = result.Find(findType, dn.Decode(flags[i]), validOnly);
 1505                            if (r.Count > 0)
 506                            {
 507                                break;
 508                            }
 509                        }
 1510                        result = r;
 511                    }
 512                    else
 513                    {
 1514                        result = result.Find(findType, arg, validOnly);
 515                    }
 516                }
 517            }
 1518        }
 519        finally
 520        {
 1521            store.Close();
 1522        }
 523
 1524        return result;
 525    }
 526
 527    private static SecureString createSecureString(string s)
 528    {
 1529        var result = new SecureString();
 1530        foreach (char ch in s)
 531        {
 1532            result.AppendChar(ch);
 533        }
 1534        return result;
 535    }
 536
 537    private bool checkPath(ref string path)
 538    {
 1539        if (File.Exists(path))
 540        {
 0541            return true;
 542        }
 543
 1544        if (_defaultDir.Length > 0 && !isAbsolutePath(path))
 545        {
 1546            string s = _defaultDir + Path.DirectorySeparatorChar + path;
 1547            if (File.Exists(s))
 548            {
 1549                path = s;
 1550                return true;
 551            }
 552        }
 0553        return false;
 554    }
 555
 556    private readonly Ice.Communicator _communicator;
 557
 558    // CA2213: _logger is borrowed from the communicator; the communicator owns its lifecycle.
 559#pragma warning disable CA2213
 560    private readonly Ice.Logger _logger;
 561#pragma warning restore CA2213
 562    private readonly int _securityTraceLevel;
 563    private readonly string _securityTraceCategory;
 564    private string _defaultDir;
 565    private bool _checkCertName;
 566    private int _verifyPeer;
 567    private int _checkCRL;
 568    private X509Certificate2Collection _certs;
 569    private bool _useMachineContext;
 570    private X509Certificate2Collection _caCerts;
 571    private readonly TrustManager _trustManager;
 572}