예제 #1
0
        /// <summary>
        /// Parses a certificate and private key from PEM encoded text.
        /// </summary>
        /// <param name="pemCombined">The PEM encoded certificate and private key.</param>
        /// <returns>The parsed <see cref="TlsCertificate"/>.</returns>
        /// <exception cref="FormatException">Thrown if the certificate could not be parsed.</exception>
        public static TlsCertificate Parse(string pemCombined)
        {
            var certificate = new TlsCertificate(pemCombined);

            certificate.Parse();

            return(certificate);
        }
예제 #2
0
        /// <summary>
        /// Attempts to parse a certificate and private key from PEM encoded text.
        /// </summary>
        /// <param name="pemCombined">The PEM encoded certificate and private key.</param>
        /// <param name="certificate">Returns as the parsed certificate.</param>
        /// <returns><c>true</c> if the certificate was parsed successfully.</returns>
        public static bool TryParse(string pemCombined, out TlsCertificate certificate)
        {
            try
            {
                certificate = TlsCertificate.Parse(pemCombined);

                return(true);
            }
            catch
            {
                certificate = default(TlsCertificate);

                return(false);
            }
        }
예제 #3
0
        /// <summary>
        /// Returns a deep copy of the instance.
        /// </summary>
        /// <returns>The clone.</returns>
        public TlsCertificate Clone()
        {
            var clone =
                new TlsCertificate()
            {
                CertPem    = this.CertPem,
                KeyPem     = this.KeyPem,
                Thumbprint = this.Thumbprint,
                ValidFrom  = this.ValidFrom,
                ValidUntil = this.ValidUntil
            };

            foreach (var host in this.Hosts)
            {
                clone.Hosts.Add(host);
            }

            return(clone);
        }
예제 #4
0
        /// <summary>
        /// Verifies a certificate file.
        /// </summary>
        /// <param name="path">Path to the certificate.</param>
        /// <exception cref="ArgumentException">Thrown if the certificate is not valid.</exception>
        public static void Validate(string path)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(path));

            var certificate = TlsCertificate.Load(path);

            // We're going to split the certificate into two files, the issued
            // certificate and the certificate authority's certificate chain
            // (AKA the CA bundle).

            var tempCertPath = Path.GetTempFileName();
            var tempCaPath   = Path.GetTempFileName();
            var tool         = "openssl";

            try
            {
                var pos = certificate.CertPem.IndexOf("-----END CERTIFICATE-----");

                if (pos == -1)
                {
                    throw new ArgumentNullException("The certificate is not formatted properly.");
                }

                pos = certificate.CertPem.IndexOf("-----BEGIN CERTIFICATE-----", pos);

                var issuedCert = certificate.CertPem.Substring(0, pos);
                var caBundle   = certificate.CertPem.Substring(pos);

                File.WriteAllText(tempCertPath, issuedCert);
                File.WriteAllText(tempCaPath, caBundle);

                var sbArgs = new StringBuilder();

                // We're going to use [certutil] for Windows and [OpenSSL]
                // for everything else.

                if (NeonHelper.IsWindows)
                {
                    tool = "certutil";

                    sbArgs.Append("-verify ");
                    sbArgs.Append($"\"{tempCertPath}\" ");
                    sbArgs.Append($"\"{tempCaPath}\"");

                    var result = NeonHelper.ExecuteCapture("certutil", sbArgs.ToString());

                    if (result.ExitCode != 0)
                    {
                        throw new ArgumentException("Invalid certificate.");
                    }
                }
                else
                {
                    sbArgs.Append("verify ");
                    sbArgs.Append("-purpose sslserver ");
                    sbArgs.Append($"-CAfile \"{tempCaPath}\" ");
                    sbArgs.Append($"\"{tempCertPath}\"");

                    var result = NeonHelper.ExecuteCapture("openssl", sbArgs.ToString());

                    if (result.ExitCode != 0)
                    {
                        throw new ArgumentException("Invalid certificate.");
                    }
                }
            }
            catch (Win32Exception)
            {
                throw new ArgumentException($"INTERNAL ERROR: Cannot find the [{tool}] SSL certificate utility on the PATH.");
            }
            finally
            {
                File.Delete(tempCertPath);
                File.Delete(tempCaPath);
            }
        }
예제 #5
0
        /// <summary>
        /// Generates a self-signed certificate for arbitrary hostnames, possibly including
        /// hostnames with wildcards.
        /// </summary>
        /// <param name="hostnames">
        /// <para>
        /// The certificate hostnames.
        /// </para>
        /// <note>
        /// You can use include a <b>"*"</b> to specify a wildcard
        /// certificate like: <b>*.test.com</b>.
        /// </note>
        /// </param>
        /// <param name="bitCount">The certificate key size in bits: one of <b>1024</b>, <b>2048</b>, or <b>4096</b> (defaults to <b>2048</b>).</param>
        /// <param name="validDays">
        /// The number of days the certificate will be valid.  This defaults to 365,000 days
        /// or about 1,000 years.
        /// </param>
        /// <param name="issuedBy">Optionally specifies the issuer.</param>
        /// <param name="issuedTo">Optionally specifies who/what the certificate is issued for.</param>
        /// <returns>The new <see cref="TlsCertificate"/>.</returns>
        public static TlsCertificate CreateSelfSigned(
            IEnumerable <string> hostnames,
            int bitCount    = 2048,
            int validDays   = 365000,
            string issuedBy = null,
            string issuedTo = null)
        {
            Covenant.Requires <ArgumentNullException>(hostnames != null && hostnames.Count() > 0);
            Covenant.Requires <ArgumentException>(bitCount == 1024 || bitCount == 2048 || bitCount == 4096);
            Covenant.Requires <ArgumentException>(validDays > 1);

            var tempFolder   = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
            var certPath     = Path.Combine(tempFolder, "cache.crt");
            var keyPath      = Path.Combine(tempFolder, "cache.key");
            var combinedPath = Path.Combine(tempFolder, "combined.pem");

            if (string.IsNullOrEmpty(issuedBy))
            {
                issuedBy = ".";
            }

            if (string.IsNullOrEmpty(issuedTo))
            {
                issuedTo = hostnames.First();
            }

            Directory.CreateDirectory(tempFolder);

            try
            {
                // We need to specify [Subject Alternative Names]
                // because specifying the hostname as the [Common Name]
                // is deprecated by the IETF and CA/Browser Forums.
                //
                // The latest OpenSSL release candidate for (v1.1.1) includes
                // a new command line option for this but the current release
                // does not, so we're going to generate a temporary config
                // file specifiying this.

                var configPath = Path.Combine(tempFolder, "cert.conf");
                var sbConfig   = new StringBuilder();

                sbConfig.Append(
                    $@"
[req]
default_bits       = 2048
prompt             = no
default_md         = sha256
distinguished_name = dn
req_extensions     = req_v3

[dn]
C=US
ST=.
L=.
O=.
OU=.
CN={issuedTo}

[req_v3]
basicConstraints       = critical, CA:TRUE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always, issuer:always
keyUsage               = critical, cRLSign, digitalSignature, keyCertSign, keyEncipherment
subjectAltName         = @alt_names

[alt_names]
");
                var hostnameList = hostnames.ToList();

                for (int i = 0; i < hostnameList.Count; i++)
                {
                    sbConfig.AppendLine($"DNS.{i + 1} = {hostnameList[i]}");
                }

                sbConfig.AppendLine();

                File.WriteAllText(configPath, NeonHelper.ToLinuxLineEndings(sbConfig.ToString()));

                var result = NeonHelper.ExecuteCapture("openssl",
                                                       $"req -newkey rsa:{bitCount} -nodes -sha256 -x509 -days {validDays} " +
                                                       $"-subj \"/C=--/ST=./L=./O=./CN=.\" " +
                                                       $"-extensions req_v3 " +
                                                       $"-keyout \"{keyPath}\" " +
                                                       $"-out \"{certPath}\" " +
                                                       $"-config \"{configPath}\"");

                if (result.ExitCode != 0)
                {
                    throw new Exception($"Certificate Error: {result.ErrorText}");
                }

                var certPem     = File.ReadAllText(certPath) + File.ReadAllText(keyPath);
                var certificate = TlsCertificate.Parse(certPem);

                return(certificate);
            }
            catch (Win32Exception e)
            {
                throw new Exception($"Cannot find the [openssl] utility on the PATH.", e);
            }
            finally
            {
                Directory.Delete(tempFolder, true);
            }
        }