/// <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); }
/// <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); } }
/// <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); }
/// <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); } }
/// <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); } }