Skip to content

Commit

Permalink
Merge pull request #2 from WouterTinus/master
Browse files Browse the repository at this point in the history
Merge master to win-acme branch
  • Loading branch information
WouterTinus authored Dec 11, 2019
2 parents ebd6e12 + e0a473d commit d99211c
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 68 deletions.
10 changes: 10 additions & 0 deletions src/PKISharp.SimplePKI/PkiCertificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public BclCertificate ToBclCertificate()
return new BclCertificate(NativeCertificate.GetEncoded());
}

public static PkiCertificate From(BclCertificate bclCert)
{
var derEncoding = System.Security.Cryptography.X509Certificates.X509ContentType.Cert;
var der = bclCert.Export(derEncoding);
return new PkiCertificate
{
NativeCertificate = new X509CertificateParser().ReadCertificate(der),
};
}

public byte[] Export(PkiEncodingFormat format)
{
switch (format)
Expand Down
2 changes: 1 addition & 1 deletion src/examples/ACMECLI/ACMECLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<ItemGroup>
<ProjectReference Include="..\..\ACMESharp\ACMESharp.csproj" />
<ProjectReference Include="..\..\ACMESharp.DotNetCore\ACMESharp.DotNetCore.csproj" />
<ProjectReference Include="..\..\PKISharp.SimplePKI\PKISharp.SimplePKI.csproj" />
<ProjectReference Include="..\Examples.Common.PKI\Examples.Common.PKI.csproj" />
<ProjectReference Include="..\Examples.Common\Examples.Common.csproj" />
</ItemGroup>
Expand Down
104 changes: 79 additions & 25 deletions src/examples/ACMECLI/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand All @@ -9,17 +8,15 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ACMESharp;
using ACMESharp.Authorizations;
using ACMESharp.Crypto;
using ACMESharp.Crypto.JOSE;
using ACMESharp.Protocol;
using ACMESharp.Protocol.Messages;
using ACMESharp.Protocol.Resources;
using Examples.Common;
using Examples.Common.PKI;
using McMaster.Extensions.CommandLineUtils;
using Newtonsoft.Json;
using PKISharp.SimplePKI;

namespace ACMECLI
{
Expand All @@ -29,7 +26,7 @@ class Program
[Option(ShortName = "", Description = "Directory to store stateful information; defaults to current")]
public string State { get; } = ".";

[Option(ShortName = "", Description = "Name of a predefined ACME CA base endpoint")]
[Option(ShortName = "", Description = "Name of a predefined ACME CA base endpoint (specify invalid value to see list)")]
[AllowedValues(
Constants.LetsEncryptName,
Constants.LetsEncryptStagingName,
Expand Down Expand Up @@ -114,9 +111,11 @@ class Program
[Option(ShortName = "", Description = "Save the certificate chain (PEM) to the named file path")]
public string ExportCert { get; }

[Option(ShortName = "", Description = "Save the certificate chain and private key (PKCS12) to the named file path")]
[Option(ShortName = "", Description = "Save the certificate chain as PFX (PKCS12) to the named file path")]
public string ExportPfx { get; }


[Option(ShortName = "", Description = "Includes the private key to the PFX (PKCS12) and secures with specified password (use ' ' for no password)")]
public string ExportPfxPassword { get; }


private string _statePath;
Expand Down Expand Up @@ -172,7 +171,7 @@ public async Task OnExecute()
}

IJwsTool accountSigner = default;
AccountKey accountKey = default;
ExamplesAccountKey accountKey = default;
string accountKeyHash = default;
if (LoadStateInto(ref accountKey, failThrow: false,
Constants.AcmeAccountKeyFile))
Expand Down Expand Up @@ -213,7 +212,7 @@ public async Task OnExecute()

account = await _acme.CreateAccountAsync(Email.Select(x => "mailto:" + x), AcceptTos);
accountSigner = _acme.Signer;
accountKey = new AccountKey
accountKey = new ExamplesAccountKey
{
KeyType = accountSigner.JwsAlg,
KeyExport = accountSigner.Export(),
Expand Down Expand Up @@ -425,6 +424,9 @@ void AddStatusCount(string status, int add)
throw new Exception("Cannot finalize Order until all Authorizations are valid");
}

PkiKeyPair keyPair = null;
PkiCertificateSigningRequest csr = null;

string certKeys = null;
byte[] certCsr = null;

Expand All @@ -441,23 +443,19 @@ void AddStatusCount(string status, int add)
switch (KeyAlgor)
{
case Constants.RsaKeyType:
certKeys = CryptoHelper.Rsa.GenerateKeys(KeySize ?? Constants.DefaultAlgorKeySizeMap[KeyAlgor]);
using (var rsa = CryptoHelper.Rsa.GenerateAlgorithm(certKeys))
{
certCsr = CryptoHelper.Rsa.GenerateCsr(Dns, rsa);
}
keyPair = PkiKeyPair.GenerateRsaKeyPair(KeySize ?? Constants.DefaultAlgorKeySizeMap[KeyAlgor]);
break;
case Constants.EcKeyType:
certKeys = CryptoHelper.Ec.GenerateKeys(KeySize ?? Constants.DefaultAlgorKeySizeMap[KeyAlgor]);
using (var ec = CryptoHelper.Ec.GenerateAlgorithm(certKeys))
{
certCsr = CryptoHelper.Ec.GenerateCsr(Dns, ec);
}
keyPair = PkiKeyPair.GenerateEcdsaKeyPair(KeySize ?? Constants.DefaultAlgorKeySizeMap[KeyAlgor]);
break;
default:
throw new Exception($"Unknown key algorithm type [{KeyAlgor}]");
}

csr = GenerateCsr(Dns, keyPair);
certKeys = Save(keyPair);
certCsr = csr.ExportSigningRequest(PkiEncodingFormat.Der);

SaveStateFrom(certKeys, Constants.AcmeOrderCertKeyFmt, orderId);
SaveStateFrom(certCsr, Constants.AcmeOrderCertCsrFmt, orderId);
}
Expand Down Expand Up @@ -535,12 +533,37 @@ void AddStatusCount(string status, int add)

if (ExportPfx != null)
{
Console.WriteLine("Exporting Certificate as PKCS12...");
using (var cert = new X509Certificate2(LoadRaw<byte[]>(
true, Constants.AcmeOrderCertFmt, orderId)))
Console.WriteLine("Exporting Certificate as PFX (PKCS12)...");

PkiKey privateKey = null;
var pfxPassword = ExportPfxPassword;
if (pfxPassword != null)
{
await File.WriteAllBytesAsync(ExportPfx,
cert.Export(X509ContentType.Pkcs12));
Console.WriteLine("...including private key in export");
string certKeys = default;
LoadStateInto(ref certKeys, failThrow: true,
Constants.AcmeOrderCertKeyFmt, orderId);
var keyPair = Load(certKeys);
privateKey = keyPair.PrivateKey;
if (pfxPassword == " ")
{
Console.WriteLine("...WITH NO PASSWORD");
pfxPassword = null;
}
else
{
Console.WriteLine("...securing with password");
}
}

using (var cert = new X509Certificate2(LoadRaw<byte[]>(true, Constants.AcmeOrderCertFmt, orderId)))
{
var pkiCert = PkiCertificate.From(cert);
var pfx = pkiCert.Export(PkiArchiveFormat.Pkcs12,
privateKey: privateKey,
password: pfxPassword?.ToCharArray());

await File.WriteAllBytesAsync(ExportPfx, pfx);
}
}
}
Expand Down Expand Up @@ -574,7 +597,8 @@ private async Task ProcessDns01(IJwsTool accountSigner, Authorization authz,
while (true)
{
string err = null;
var dnsValues = (await DnsUtil.LookupRecordAsync(dnsCd.DnsRecordType, dnsCd.DnsRecordName)).Select(x => x.Trim('"'));
var lookup = await DnsUtil.LookupRecordAsync(dnsCd.DnsRecordType, dnsCd.DnsRecordName);
var dnsValues = lookup?.Select(x => x.Trim('"'));
if (dnsValues == null)
{
err = "Could not resolve *any* DNS entries for Challenge record name";
Expand Down Expand Up @@ -757,5 +781,35 @@ private string ComputeHash(string value)
return BitConverter.ToString(hash).Replace("-", "");
}
}

private string Save(PkiKeyPair keyPair)
{
using (var ms = new MemoryStream())
{
keyPair.Save(ms);
return Convert.ToBase64String(ms.ToArray());
}
}

private PkiKeyPair Load(string b64)
{
using (var ms = new MemoryStream(Convert.FromBase64String(b64)))
{
return PkiKeyPair.Load(ms);
}
}

private PkiCertificateSigningRequest GenerateCsr(IEnumerable<string> dnsNames,
PkiKeyPair keyPair)
{
var firstDns = dnsNames.First();
var csr = new PkiCertificateSigningRequest($"CN={firstDns}", keyPair,
PkiHashAlgorithm.Sha256);

csr.CertificateExtensions.Add(
PkiCertificateExtension.CreateDnsSubjectAlternativeNames(dnsNames));

return csr;
}
}
}
54 changes: 28 additions & 26 deletions src/examples/ACMECLI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,31 @@ The tool provides for the following parameters and options:
Usage: ACMECLI [options]

Options:
--state <STATE> Directory to store stateful information; defaults to current
--ca-name <CA_NAME> Name of a predefined ACME CA base endpoint
--ca-url <CA_URL> Full URL of an ACME CA endpoint; this option overrides CaName
--refresh-dir Flag indicates to refresh the current cached ACME Directory of service endpoints for the target CA
--email <EMAIL> One or more emails to be registered as account contact info (can be repeated)
--accept-tos Flag indicates that you agree to CA's terms of service
--dns <DNS> One or more DNS names to include in the cert; the first is primary subject name, subsequent are subject alternative names (can be repeated)
--refresh-order Flag indicates to refresh the state of pending ACME Order
--challenge-type <CHALLENGE_TYPE> Indicates that only one specific Challenge type should be handled
--refresh-challenges Flag indicates to refresh the state of the Challenges of the pending ACME Order
--test-challenges Flag indicates to check if the Challenges have been handled correctly
--wait-for-test:<WAIT_FOR_TEST> Flag indicates to wait until Challenge tests are successfully validated, optionally override the default timeout of 300 (seconds)
--answer-challenges Flag indicates to submit Answers to pending Challenges
--wait-for-authz:<WAIT_FOR_AUTHZ> Flag indicates to wait until Authorizations become Valid, optionally override the default timeout of 300 (seconds)
--finalize Flag indicates to finalize the pending ACME Order
--key-algor <KEY_ALGOR> Indicates the encryption algorithm of certificate keys, defaults to RSA
--key-size <KEY_SIZE> Indicates the encryption algorithm key size, defaults to 2048 (RSA) or 256 (EC)
--regenerate-csr Flag indicates to regenerate a certificate key pair and CSR
--refresh-cert Flag indicates to refresh the local cache of an issued certificate
--wait-for-cert:<WAIT_FOR_CERT> Flag indicates to wait until Certificate is available, optionally override the default timeout of 300 (seconds)
--export-cert <EXPORT_CERT> Save the certificate chain (PEM) to the named file path
--export-pfx <EXPORT_PFX> Save the certificate chain and private key (PKCS12) to the named file path
-?|-h|--help Show help information
--state <STATE> Directory to store stateful information; defaults to current
--ca-name <CA_NAME> Name of a predefined ACME CA base endpoint (specify invalid value to see list)
--ca-url <CA_URL> Full URL of an ACME CA endpoint; this option overrides CaName
--refresh-dir Flag indicates to refresh the current cached ACME Directory of service endpoints for the target CA
--email <EMAIL> One or more emails to be registered as account contact info (can be repeated)
--accept-tos Flag indicates that you agree to CA's terms of service
--dns <DNS> One or more DNS names to include in the cert; the first is primary subject name, subsequent are subject alternative names (can be repeated)
--name-server <NAME_SERVER> One or more DNS name servers to be used to resolve host entries, such as during testing (can be repeated)
--refresh-order Flag indicates to refresh the state of pending ACME Order
--challenge-type <CHALLENGE_TYPE> Indicates that only one specific Challenge type should be handled
--refresh-challenges Flag indicates to refresh the state of the Challenges of the pending ACME Order
--test-challenges Flag indicates to check if the Challenges have been handled correctly
--wait-for-test[:<WAIT_FOR_TEST>] Flag indicates to wait until Challenge tests are successfully validated, optionally override the default timeout of 300 (seconds)
--answer-challenges Flag indicates to submit Answers to pending Challenges
--wait-for-authz[:<WAIT_FOR_AUTHZ>] Flag indicates to wait until Authorizations become Valid, optionally override the default timeout of 300 (seconds)
--finalize Flag indicates to finalize the pending ACME Order
--key-algor <KEY_ALGOR> Indicates the encryption algorithm of certificate keys, defaults to RSA
--key-size <KEY_SIZE> Indicates the encryption algorithm key size, defaults to 2048 (RSA) or 256 (EC)
--regenerate-csr Flag indicates to regenerate a certificate key pair and CSR
--refresh-cert Flag indicates to refresh the local cache of an issued certificate
--wait-for-cert[:<WAIT_FOR_CERT>] Flag indicates to wait until Certificate is available, optionally override the default timeout of 300 (seconds)
--export-cert <EXPORT_CERT> Save the certificate chain (PEM) to the named file path
--export-pfx <EXPORT_PFX> Save the certificate chain as PFX (PKCS12) to the named file path
--export-pfx-password <EXPORT_PFX_PASSWORD> Includes the private key to the PFX (PKCS12) and secures with specified password (use ' ' for no password)
-?|-h|--help Show help information
```
You can invoke it piecemeal and complete each step independently or you can combine all the
Expand Down Expand Up @@ -94,9 +96,9 @@ private key and generate the CSR to submit to the CA.
```
Finally, save the complete certificate chain and corresponding
private key to a PKCS#12 format file.
private key to a PKCS#12 format file with NO password.
```shell
> acmecli --dns myapp.example.com --dns myapp-0.example.com --dns myapp-1.example.com --export-pfx mycertificate.pfx
> acmecli --dns myapp.example.com --dns myapp-0.example.com --dns myapp-1.example.com --export-pfx mycertificate.pfx --export-pfx-password " "
```
## Invoke in _One Fell Swoop_
Expand All @@ -107,7 +109,7 @@ either the ACME CA or from your actions, i.e. by completing the Challenges.
```shell
## In this case all the DNS Identifiers are wildcards, so the
## CA will only issue DNS type Challenges as per the ACME spec
> acmecli --email [email protected] --email [email protected] --accept-tos --dns *.example.com --dns *.example.net --test-challenges --wait-for-test:600 --answer-challenges --wait-for-authz --finalize --key-algor ec --key-size 256 --wait-for-cert --export-cert my-example.pem --export-pfx my-example.pfx
> acmecli --email [email protected] --email [email protected] --accept-tos --dns *.example.com --dns *.example.net --test-challenges --wait-for-test:600 --answer-challenges --wait-for-authz --finalize --key-algor ec --key-size 256 --wait-for-cert --export-cert my-example.pem --export-pfx my-example.pfx --export-pfx-password " "
```
With the single command above:
Expand Down
4 changes: 2 additions & 2 deletions src/examples/ACMEKestrel/AcmeHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public Task StartAsync(CancellationToken cancellationToken)

(_, _state.ServiceDirectory) = Load<ServiceDirectory>(_state.ServiceDirectoryFile);
(_, _state.Account) = Load<AccountDetails>(_state.AccountFile);
(_, _state.AccountKey) = Load<AccountKey>(_state.AccountKeyFile);
(_, _state.AccountKey) = Load<ExamplesAccountKey>(_state.AccountKeyFile);
(_, _state.Order) = Load<OrderDetails>(_state.OrderFile);
(_, _state.Authorizations) = Load<Dictionary<string, Authorization>>(
_state.AuthorizationsFile);
Expand Down Expand Up @@ -153,7 +153,7 @@ protected async Task<bool> ResolveAccount(AcmeProtocolClient acme)
_state.Account = await acme.CreateAccountAsync(
contacts: contacts,
termsOfServiceAgreed: _options.AcceptTermsOfService);
_state.AccountKey = new AccountKey
_state.AccountKey = new ExamplesAccountKey
{
KeyType = acme.Signer.JwsAlg,
KeyExport = acme.Signer.Export(),
Expand Down
2 changes: 1 addition & 1 deletion src/examples/ACMEKestrel/AcmeState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class AcmeState

public string AccountKeyFile { get; set; }

public AccountKey AccountKey { get; set; }
public ExamplesAccountKey AccountKey { get; set; }

public string OrderFile { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Examples.Common.PKI
{
public class AccountKey
public class ExamplesAccountKey
{
public string KeyType { get; set; }
public string KeyExport { get; set; }
Expand Down
18 changes: 16 additions & 2 deletions test/ACMESharp.IntegrationTests/AcmeAccountTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@

namespace ACMESharp.IntegrationTests
{
[Collection(nameof(AcmeAccountWithPostAsGetTests))]
[CollectionDefinition(nameof(AcmeAccountWithPostAsGetTests))]
[TestOrder(0_051)]
public class AcmeAccountWithPostAsGetTests : AcmeAccountTests
{
public AcmeAccountWithPostAsGetTests(ITestOutputHelper output, StateFixture state, ClientsFixture clients)
: base(output, state, clients)
{
_usePostAsGet = true;
}
}

[Collection(nameof(AcmeAccountTests))]
[CollectionDefinition(nameof(AcmeAccountTests))]
[TestOrder(0_05)]
[TestOrder(0_050)]
public class AcmeAccountTests : IntegrationTest,
IClassFixture<StateFixture>,
IClassFixture<ClientsFixture>
{
protected bool _usePostAsGet = false;

public AcmeAccountTests(ITestOutputHelper output, StateFixture state, ClientsFixture clients)
: base(state, clients)
{
Expand Down Expand Up @@ -48,7 +62,7 @@ public void InitAcmeClient()
{
BaseAddress = Clients.BaseAddress
};
Clients.Acme = new AcmeProtocolClient(Clients.Http);
Clients.Acme = new AcmeProtocolClient(Clients.Http, usePostAsGet: _usePostAsGet);
}

[Fact]
Expand Down
Loading

0 comments on commit d99211c

Please sign in to comment.