/// <summary> /// This callback validates a RIoT certificate chain or a bare Alias certificate. /// We generally don't want to validate a chain against the user or machine cert store, /// so we do it (semi) manually with X509Chain. If the device only presents the Alias /// Certificate (no chain) we call it a "bare certificate.) /// </summary> /// <param name="sender"></param> /// <param name="_certificate"></param> /// <param name="inChain"></param> /// <param name="sslPolicyErrors"></param> /// <returns></returns> public static bool ValidateDeviceCertificate(object sender, System.Security.Cryptography.X509Certificates.X509Certificate _certificate, X509Chain inChain, SslPolicyErrors sslPolicyErrors) { //ValidateBareCertificateWithBcrypt(new X509Certificate2(_certificate)); if (_certificate == null) { // not sure that this can happen Helpers.Notify($"No certificate presented by client", true); return(false); } // This is the leaf (alias) certificate X509Certificate2 certificate = new X509Certificate2(_certificate); // Did the device just send one certificate? If so, we don't check the chain: we just // check that the certificate was signed by a known DeviceID in the routine below. // Note that if the built-in chain builder found certificates in the system store to build // chain, then inChain will contain those and the processing will follow the "chain" rules // rather than the bare-certificate validation rules. if (inChain.ChainElements.Count == 1) { Helpers.Notify($"count==1", true); return(ValidateBareCertificate(certificate)); } // Else we have a chain... // We need at least 3 certificates: // Alias // DeviceID (issued by vendor) // [zero or any number of intermediate CA] // Device Vendor CA int chainLength = inChain.ChainElements.Count; if (chainLength < 3) { Helpers.Notify($"Chain length too short: {chainLength}", true); return(false); } Helpers.Notify($"Device presented a certificate chain of length {inChain.ChainElements.Count}"); // Put the device-provided chain in a new X509Chain so that we can validate it X509Chain chain = new X509Chain(false); // todo: this seems like a reasonable starting point for the flags chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // Note: this flag seems to be ignored. In any case, we don't use the // machine or user-context cert store: we check the root against DeviceCA // provided as a parameter during class instantiation (or a database of authorized // CAs in the final service.) chain.ChainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority; // Add the intermediate and root-CA certs that came out of the TLS session. foreach (var c in inChain.ChainElements) { chain.ChainPolicy.ExtraStore.Add(c.Certificate); } // Can we build a chain using the leaf-cert and the rest of the provided certs? bool valid = chain.Build(new X509Certificate2(certificate)); if (!valid) { Helpers.Notify($"Chain building failed: {chainLength}", true); foreach (var err in chain.ChainStatus) { Helpers.Notify($" Error:{err.StatusInformation.ToString()}", true); } return(false); } // Get the chain we built. Regardless of what the client sent, we // want a chain of 3 or more certificates (alias, DevID, [...], CA) in our // chain. var deviceCertChain = chain.ChainElements; if (deviceCertChain.Count < 3) { Helpers.Notify($"Chain length too short: {deviceCertChain.Count}", true); return(false); } // Is the root one of the registered CAs for this DRS instance? // Here we just recognize a single root var thisDeviceRootCert = deviceCertChain[deviceCertChain.Count - 1]; if (thisDeviceRootCert.Certificate.Thumbprint != DeviceCA.Thumbprint) { Helpers.Notify($"Device not certified by a known/authorized CA", true); return(false); } // Next, extract the RIoT extension, and see if it is present/well-formed var aliasCert = deviceCertChain[0].Certificate; var deviceIDCert = deviceCertChain[1].Certificate; var deviceInfo = ExtensionDecoder.Decode(aliasCert); // check we have a good extension if (deviceInfo == null) { Helpers.Notify("Certificate does not have well-formed RIoT extension", true); return(false); } // Check that the DevID claimed in the RIoT extension matches that defined by the // cert chain. This is a *critical* security check if the server just wants to authenticate // based on the Alias (leaf) certificate rather than the DeviceID cert and the Alias Cert. var encodedDeviceID = deviceIDCert.PublicKey.EncodedKeyValue; if (!Helpers.ArraysAreEqual(encodedDeviceID.RawData, deviceInfo.EncodedDeviceIDKey)) { Helpers.Notify("Alias Certificate DeviceID does not match DeviceID certificate", true); return(false); } Helpers.Notify("All RIoT Certificate checks passed"); return(true); }