Skip to content

Commit

Permalink
Support tls-sni-01 challenges (#164)
Browse files Browse the repository at this point in the history
* Add Challenge Type to UI

* minor fixes and refactoring

* implement tls-sni-01 challenge

* reset ServerCertificateValidationCallback after use

* Fix CheckSNI, Update CheckURL

Uses hosts file to enable SNI checking in CheckSNI (see https://github.com/dotnet/corefx/issues/17852#issuecomment-332939829)
Add logging to CheckURL, to return debug info to the user in case of failure

* Refactor Challenge Response Testing

Pulls out the testing of challenge responses from the real request flow to allow response simulation.

Implements simulated tests for tls-sni-01 and http-01

* Add Simulated Challenge Response Testing to UI

Allows the user to test simulated challenge response validation without having to make a real ACME challenge.

Useful for testing configuration/permissiions without "burning" real request limit quota.

* Change ACMESharp submodule to track tls-sni branch until PR is accepted
  • Loading branch information
Marcus-L authored and webprofusion-chrisc committed Oct 2, 2017
1 parent 9f084a0 commit 191bfe6
Show file tree
Hide file tree
Showing 16 changed files with 652 additions and 213 deletions.
3 changes: 2 additions & 1 deletion .gitmodules
@@ -1,3 +1,4 @@
[submodule "src/lib/ACMESharp"]
path = src/lib/ACMESharp
url = git://github.com/ebekker/ACMESharp.git
url = https://github.com/Marcus-L/ACMESharp.git
branch = tls-sni
16 changes: 16 additions & 0 deletions src/Certify.Core/ACMESharpCompat/ACMESharpUtils.cs
Expand Up @@ -32,6 +32,22 @@ namespace Certify.ACMESharpCompat
/// </summary>
public static class ACMESharpUtils
{
/// <summary>
/// Identifier validation challenge type indicator for
/// <see cref="https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.5">DNS</see>.
/// </summary>
public static readonly string CHALLENGE_TYPE_DNS = "dns-01";
/// <summary>
/// Identifier validation challenge type indicator for
/// <see cref="https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.2">HTTP (non-SSL/TLS)</see>.
/// </summary>
public const string CHALLENGE_TYPE_HTTP = "http-01";
/// <summary>
/// Identifier validation challenge type indicator for
/// <see cref="https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3">TLS SNI</see>.
/// </summary>
public const string CHALLENGE_TYPE_SNI = "tls-sni-01";

public const string WELL_KNOWN_LE = "LetsEncrypt";

public const string WELL_KNOWN_LESTAGE = "LetsEncrypt-STAGING";
Expand Down
10 changes: 10 additions & 0 deletions src/Certify.Core/Management/APIProviders/ACMESharpProvider.cs
Expand Up @@ -89,6 +89,11 @@ public string GetVaultSummary()
return _vaultManager.GetVaultPath();
}

public string GetActionSummary()
{
return _vaultManager.GetActionLogSummary();
}

public void EnableSensitiveFileEncryption()
{
_vaultManager.UseEFSForSensitiveFiles = true;
Expand All @@ -113,6 +118,11 @@ public PendingAuthorization PerformIISAutomatedChallengeResponse(IISManager iisM
return processedAuth;
}

public async Task<APIResult> TestChallengeResponse(IISManager iisManager, ManagedSite managedSite)
{
return await _vaultManager.TestChallengeResponse(iisManager, managedSite);
}

public void SubmitChallenge(string domainIdentifierId, string challengeType)
{
_vaultManager.SubmitChallenge(domainIdentifierId, challengeType);
Expand Down
11 changes: 11 additions & 0 deletions src/Certify.Core/Management/APIProviders/CertesProvider.cs
Expand Up @@ -53,6 +53,12 @@ public string GetVaultSummary()
return null;
}

public string GetActionSummary()
{
System.Diagnostics.Debug.WriteLine("Certes: GetActionSummary not implemented");
return null;
}

public void EnableSensitiveFileEncryption()
{
throw new NotImplementedException();
Expand Down Expand Up @@ -101,6 +107,11 @@ public PendingAuthorization PerformIISAutomatedChallengeResponse(IISManager iisM
throw new NotImplementedException();
}

public Task<APIResult> TestChallengeResponse(IISManager iISManager, ManagedSite managedSite)
{
throw new NotImplementedException();
}

public void SubmitChallenge(string domainIdentifierId, string challengeType)
{
throw new NotImplementedException();
Expand Down
101 changes: 92 additions & 9 deletions src/Certify.Core/Management/CertificateManager.cs
@@ -1,36 +1,119 @@
using System;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Extension;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace Certify.Management
{
public class CertificateManager
public static class CertificateManager
{
public X509Certificate2 GetCertificate(string filename)
public static X509Certificate2 GenerateTlsSni01Certificate(string domain)
{
// configure generators
var random = new SecureRandom(new CryptoApiRandomGenerator());
var keyGenerationParameters = new KeyGenerationParameters(random, 2048);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);

// create self-signed certificate
var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
var certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.SetSubjectDN(new X509Name($"CN={domain}"));
certificateGenerator.SetIssuerDN(new X509Name($"CN={domain}"));
certificateGenerator.SetSerialNumber(serialNumber);
certificateGenerator.SetNotBefore(DateTime.UtcNow);
certificateGenerator.SetNotAfter(DateTime.UtcNow.AddMinutes(5));
certificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName.Id, false, new DerSequence(new Asn1Encodable[] { new GeneralName(GeneralName.DnsName, domain) }));
certificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeID[] { KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPClientAuth }));
certificateGenerator.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.KeyEncipherment | KeyUsage.DigitalSignature));

var keyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(keyPair.Public);
var bouncy_cert = certificateGenerator.Generate(new Asn1SignatureFactory("SHA256WithRSA", keyPair.Private, random));

// get private key into machine key store
var csp = new RSACryptoServiceProvider(
new CspParameters
{
KeyContainerName = Guid.NewGuid().ToString(),
KeyNumber = 1,
Flags = CspProviderFlags.UseMachineKeyStore
});
var rp = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)keyPair.Private);
csp.ImportParameters(rp);

// convert from bouncy cert to X509Certificate2
return new X509Certificate2(bouncy_cert.GetEncoded(), (string)null, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet)
{
FriendlyName = domain,
PrivateKey = csp
};
}

public static bool VerifyCertificateSAN(System.Security.Cryptography.X509Certificates.X509Certificate certificate, string sni)
{
// check subject alternate name (must have exactly 1, equal to sni)
var x509 = DotNetUtilities.FromX509Certificate(certificate);
var sans = X509ExtensionUtilities.GetSubjectAlternativeNames(x509);
if (sans.Count != 1) return false;
var san = (System.Collections.IList)((System.Collections.IList)sans)[0];
var sniOK = san[0].Equals(GeneralName.DnsName) && san[1].Equals(sni);

// if subject matches sni and SAN is ok, return true
return x509.SubjectDN.ToString() == $"CN={sni}" && sniOK;
}

public static X509Certificate2 LoadCertificate(string filename)
{
var cert = new X509Certificate2();
cert.Import(filename);
return cert;
}

public X509Certificate2 StoreCertificate(string host, string pfxFile)
public static X509Certificate2 StoreCertificate(string host, string pfxFile)
{
var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);
//TODO: remove old cert?
var certificate = new X509Certificate2(pfxFile, "", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
certificate.GetExpirationDateString();
certificate.FriendlyName = host + " [Certify] - " + certificate.GetEffectiveDateString() + " to " + certificate.GetExpirationDateString();

return StoreCertificate(certificate);
}

public static X509Certificate2 StoreCertificate(X509Certificate2 certificate)
{
var store = GetDefaultStore();
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);
//TODO: remove old cert?
store.Add(certificate);
store.Close();
return certificate;
}

public X509Store GetDefaultStore()
public static void RemoveCertificate(X509Certificate2 certificate)
{
var store = GetDefaultStore();
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);
store.Remove(certificate);
store.Close();
}

public static X509Store GetDefaultStore()
{
return new X509Store(StoreName.My, StoreLocation.LocalMachine);
}
Expand All @@ -41,7 +124,7 @@ public X509Store GetDefaultStore()
/// </summary>
/// <param name="certificate">The new cert to keep</param>
/// <param name="hostPrefix">The cert friendly name prefix to match certs to clean up</param>
public void CleanupCertificateDuplicates(X509Certificate2 certificate, string hostPrefix)
public static void CleanupCertificateDuplicates(X509Certificate2 certificate, string hostPrefix)
{
// TODO: remove distinction, this is legacy from the old version which didn't have a
// clear app specific prefix
Expand Down

0 comments on commit 191bfe6

Please sign in to comment.