< Summary

Information
Class: Ice.SSL.SSLEngine
Assembly: Ice
File(s): /home/runner/work/ice/ice/csharp/src/Ice/SSL/SSLEngine.cs
Tag: 71_18251537082
Line coverage
75%
Covered lines: 188
Uncovered lines: 61
Coverable lines: 249
Total lines: 544
Line coverage: 75.5%
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)

/home/runner/work/ice/ice/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("\nhash algorithm = " + stream.HashAlgorithm + "/" + stream.HashStrength);
 0179        s.Append("\ncipher algorithm = " + stream.CipherAlgorithm + "/" + stream.CipherStrength);
 0180        s.Append("\nkey exchange algorithm = " + stream.KeyExchangeAlgorithm + "/" + stream.KeyExchangeStrength);
 0181        s.Append("\nprotocol = " + stream.SslProtocol);
 0182        _logger.trace(_securityTraceCategory, s.ToString());
 0183    }
 184
 185    internal void verifyPeer(ConnectionInfo info, string description)
 186    {
 1187        if (!_trustManager.verify(info, description))
 188        {
 1189            string msg = (info.incoming ? "incoming" : "outgoing") + " connection rejected by trust manager\n" +
 1190                description;
 1191            if (_securityTraceLevel >= 1)
 192            {
 0193                _logger.trace(_securityTraceCategory, msg);
 194            }
 195
 1196            throw new SecurityException($"IceSSL: {msg}");
 197        }
 1198    }
 199
 200    internal SslClientAuthenticationOptions createClientAuthenticationOptions(
 201        RemoteCertificateValidationCallback remoteCertificateValidationCallback,
 202        string host)
 203    {
 1204        var authenticationOptions = new SslClientAuthenticationOptions
 1205        {
 1206            ClientCertificates = _certs,
 1207            LocalCertificateSelectionCallback = (sender, targetHost, certs, remoteCertificate, acceptableIssuers) =>
 1208            {
 1209                if (certs == null || certs.Count == 0)
 1210                {
 1211                    return null;
 1212                }
 1213                else if (certs.Count == 1)
 1214                {
 1215                    return certs[0];
 1216                }
 1217
 1218                // Use the first certificate that match the acceptable issuers.
 0219                if (acceptableIssuers != null && acceptableIssuers.Length > 0)
 1220                {
 0221                    foreach (X509Certificate certificate in certs)
 1222                    {
 0223                        if (Array.IndexOf(acceptableIssuers, certificate.Issuer) != -1)
 1224                        {
 0225                            return certificate;
 1226                        }
 1227                    }
 1228                }
 0229                return certs[0];
 0230            },
 1231            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1232            TargetHost = host,
 1233        };
 234
 1235        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1236        if (_caCerts is null)
 237        {
 1238            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 239        }
 240        else
 241        {
 1242            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1243            foreach (X509Certificate certificate in _caCerts)
 244            {
 1245                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 246            }
 247        }
 248
 1249        if (!_checkCertName)
 250        {
 1251            authenticationOptions.CertificateChainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreInvalidName;
 252        }
 253
 1254        if (_checkCRL == 1)
 255        {
 0256            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0257                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 258        }
 1259        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1260            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1261        return authenticationOptions;
 262    }
 263
 264    internal SslServerAuthenticationOptions createServerAuthenticationOptions(
 265        RemoteCertificateValidationCallback remoteCertificateValidationCallback)
 266    {
 267        // Get the certificate collection and select the first one.
 1268        X509Certificate2 cert = null;
 1269        if (_certs.Count > 0)
 270        {
 1271            cert = _certs[0];
 272        }
 273
 1274        var authenticationOptions = new SslServerAuthenticationOptions
 1275        {
 1276            ServerCertificate = cert,
 1277            ClientCertificateRequired = _verifyPeer > 0,
 1278            RemoteCertificateValidationCallback = remoteCertificateValidationCallback,
 1279            CertificateRevocationCheckMode = X509RevocationMode.NoCheck
 1280        };
 281
 1282        authenticationOptions.CertificateChainPolicy = new X509ChainPolicy();
 1283        if (_caCerts is null)
 284        {
 0285            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.System;
 286        }
 287        else
 288        {
 1289            authenticationOptions.CertificateChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
 1290            foreach (X509Certificate certificate in _caCerts)
 291            {
 1292                authenticationOptions.CertificateChainPolicy.CustomTrustStore.Add(certificate);
 293            }
 294        }
 1295        authenticationOptions.CertificateChainPolicy.RevocationMode =
 1296            _checkCRL == 0 ? X509RevocationMode.NoCheck : X509RevocationMode.Online;
 1297        if (_checkCRL == 1)
 298        {
 0299            authenticationOptions.CertificateChainPolicy.VerificationFlags |=
 0300                X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown;
 301        }
 1302        return authenticationOptions;
 303    }
 304
 305    private static bool isAbsolutePath(string path)
 306    {
 307        // Skip whitespace
 1308        path = path.Trim();
 309
 1310        if (AssemblyUtil.isWindows)
 311        {
 312            // We need at least 3 non-whitespace characters to have an absolute path
 0313            if (path.Length < 3)
 314            {
 0315                return false;
 316            }
 317
 318            // Check for X:\ path ('\' may have been converted to '/')
 0319            if ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'))
 320            {
 0321                return path[1] == ':' && (path[2] == '\\' || path[2] == '/');
 322            }
 323        }
 324        // Check for UNC path
 1325        return (path[0] == '\\' && path[1] == '\\') || path[0] == '/';
 326    }
 327
 328    private static X509Certificate2Collection findCertificates(
 329        string prop,
 330        StoreLocation storeLocation,
 331        string name,
 332        string value)
 333    {
 334        // Open the X509 certificate store.
 335        X509Store store;
 336        try
 337        {
 338            try
 339            {
 1340                store = new X509Store((StoreName)Enum.Parse(typeof(StoreName), name, true), storeLocation);
 1341            }
 0342            catch (ArgumentException)
 343            {
 0344                store = new X509Store(name, storeLocation);
 0345            }
 1346            store.Open(OpenFlags.ReadOnly);
 1347        }
 0348        catch (Exception ex)
 349        {
 0350            throw new Ice.InitializationException($"IceSSL: failure while opening store specified by {prop}", ex);
 351        }
 352
 353        // Start with all of the certificates in the collection and filter as necessary.
 354        //
 355        // - If the value is "*", return all certificates.
 356        // - Otherwise, search using key:value pairs. The following keys are supported:
 357        //
 358        //   Issuer
 359        //   IssuerDN
 360        //   Serial
 361        //   Subject
 362        //   SubjectDN
 363        //   SubjectKeyId
 364        //   Thumbprint
 365        //
 366        //   A value must be enclosed in single or double quotes if it contains whitespace.
 1367        X509Certificate2Collection result = [.. store.Certificates];
 368        try
 369        {
 1370            if (value != "*")
 371            {
 1372                if (!value.Contains(':', StringComparison.Ordinal))
 373                {
 1374                    throw new Ice.InitializationException($"IceSSL: no key in `{value}'");
 375                }
 1376                int start = 0;
 377                int pos;
 1378                while ((pos = value.IndexOf(':', start)) != -1)
 379                {
 380                    // Parse the X509FindType.
 1381                    string field = value[start..pos].Trim().ToUpperInvariant();
 382                    X509FindType findType;
 1383                    if (field == "SUBJECT")
 384                    {
 1385                        findType = X509FindType.FindBySubjectName;
 386                    }
 1387                    else if (field == "SUBJECTDN")
 388                    {
 1389                        findType = X509FindType.FindBySubjectDistinguishedName;
 390                    }
 1391                    else if (field == "ISSUER")
 392                    {
 1393                        findType = X509FindType.FindByIssuerName;
 394                    }
 1395                    else if (field == "ISSUERDN")
 396                    {
 1397                        findType = X509FindType.FindByIssuerDistinguishedName;
 398                    }
 1399                    else if (field == "THUMBPRINT")
 400                    {
 1401                        findType = X509FindType.FindByThumbprint;
 402                    }
 1403                    else if (field == "SUBJECTKEYID")
 404                    {
 1405                        findType = X509FindType.FindBySubjectKeyIdentifier;
 406                    }
 1407                    else if (field == "SERIAL")
 408                    {
 1409                        findType = X509FindType.FindBySerialNumber;
 410                    }
 411                    else
 412                    {
 1413                        throw new Ice.InitializationException($"IceSSL: unknown key in `{value}'");
 414                    }
 415
 416                    // Parse the argument.
 1417                    start = pos + 1;
 1418                    while (start < value.Length && (value[start] == ' ' || value[start] == '\t'))
 419                    {
 0420                        ++start;
 421                    }
 422
 1423                    if (start == value.Length)
 424                    {
 0425                        throw new Ice.InitializationException($"IceSSL: missing argument in `{value}'");
 426                    }
 427
 428                    string arg;
 1429                    if (value[start] == '"' || value[start] == '\'')
 430                    {
 1431                        int end = start;
 1432                        ++end;
 1433                        while (end < value.Length)
 434                        {
 1435                            if (value[end] == value[start] && value[end - 1] != '\\')
 436                            {
 437                                break;
 438                            }
 1439                            ++end;
 440                        }
 1441                        if (end == value.Length || value[end] != value[start])
 442                        {
 0443                            throw new Ice.InitializationException($"IceSSL: unmatched quote in `{value}'");
 444                        }
 1445                        ++start;
 1446                        arg = value[start..end];
 1447                        start = end + 1;
 448                    }
 449                    else
 450                    {
 1451                        char[] ws = [' ', '\t'];
 1452                        int end = value.IndexOfAny(ws, start);
 1453                        if (end == -1)
 454                        {
 1455                            arg = value[start..];
 1456                            start = value.Length;
 457                        }
 458                        else
 459                        {
 1460                            arg = value[start..end];
 1461                            start = end + 1;
 462                        }
 463                    }
 464
 465                    // Execute the query.
 466                    //
 467                    // TODO: allow user to specify a value for validOnly?
 1468                    bool validOnly = false;
 1469                    if (findType == X509FindType.FindBySubjectDistinguishedName ||
 1470                        findType == X509FindType.FindByIssuerDistinguishedName)
 471                    {
 1472                        X500DistinguishedNameFlags[] flags = [
 1473                            X500DistinguishedNameFlags.None,
 1474                            X500DistinguishedNameFlags.Reversed,
 1475                        ];
 1476                        var dn = new X500DistinguishedName(arg);
 1477                        X509Certificate2Collection r = result;
 1478                        for (int i = 0; i < flags.Length; ++i)
 479                        {
 1480                            r = result.Find(findType, dn.Decode(flags[i]), validOnly);
 1481                            if (r.Count > 0)
 482                            {
 483                                break;
 484                            }
 485                        }
 1486                        result = r;
 487                    }
 488                    else
 489                    {
 1490                        result = result.Find(findType, arg, validOnly);
 491                    }
 492                }
 493            }
 1494        }
 495        finally
 496        {
 1497            store.Close();
 1498        }
 499
 1500        return result;
 501    }
 502
 503    private static SecureString createSecureString(string s)
 504    {
 1505        var result = new SecureString();
 1506        foreach (char ch in s)
 507        {
 1508            result.AppendChar(ch);
 509        }
 1510        return result;
 511    }
 512
 513    private bool checkPath(ref string path)
 514    {
 1515        if (File.Exists(path))
 516        {
 0517            return true;
 518        }
 519
 1520        if (_defaultDir.Length > 0 && !isAbsolutePath(path))
 521        {
 1522            string s = _defaultDir + Path.DirectorySeparatorChar + path;
 1523            if (File.Exists(s))
 524            {
 1525                path = s;
 1526                return true;
 527            }
 528        }
 0529        return false;
 530    }
 531
 532    private readonly Ice.Communicator _communicator;
 533    private readonly Ice.Logger _logger;
 534    private readonly int _securityTraceLevel;
 535    private readonly string _securityTraceCategory;
 536    private string _defaultDir;
 537    private bool _checkCertName;
 538    private int _verifyPeer;
 539    private int _checkCRL;
 540    private X509Certificate2Collection _certs;
 541    private bool _useMachineContext;
 542    private X509Certificate2Collection _caCerts;
 543    private readonly TrustManager _trustManager;
 544}