Skip to content

Commit

Permalink
Merge branch 'preview' into beta
Browse files Browse the repository at this point in the history
  • Loading branch information
fszlin committed Mar 16, 2018
2 parents dfcebe7 + 7c3b10a commit 4a70642
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 46 deletions.
4 changes: 2 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ artifacts:

deploy:
- provider: GitHub
release: v$(CERTES_PACKAGE_VER)
description: 'Certes v$(CERTES_PACKAGE_VER)'
release: v$(CERTES_PACKAGE_VERSION)
description: 'Certes v$(CERTES_PACKAGE_VERSION)'
auth_token:
secure: B+lTI7i/tnZeg1ZSmho3HvOWjs0C4hptNy5cvWgF0Nn7b6v8nwT/mxEWVCfIJ7Fy
artifact: nupkg,cli
Expand Down
17 changes: 15 additions & 2 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@
All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- Add `Processing` status for challenges.

### Changed
- Fix `Content-Type` header for POST requests ([#76][i76])

## [2.0.0] - 2018-03-13
### Added
- [ACME v2](APIv2.md) support
- Add support for JSON web signature using ECDSA key
-

## [1.1.4] - 2018-03-04
### Changed
- Fix error when processing server response without `Content-Type` header
- Fix `full-chain-off` option for CLI

## [1.1.3] - 2017-11-23
Expand Down Expand Up @@ -34,11 +44,14 @@ All notable changes to this project will be documented in this file.
### Changed
- Fix error when parsing directory resource with *meta* property. ([#5][i5])

[Unreleased]: https://github.com/fszlin/certes/compare/v1.1.3...HEAD
[Unreleased]: https://github.com/fszlin/certes/compare/v2.0.0...HEAD
[1.1.0]: https://github.com/fszlin/certes/compare/v1.0.7...v1.1.0
[1.1.1]: https://github.com/fszlin/certes/compare/v1.1.0...v1.1.1
[1.1.2]: https://github.com/fszlin/certes/compare/v1.1.1...v1.1.2
[1.1.3]: https://github.com/fszlin/certes/compare/v1.1.2...v1.1.3
[1.1.4]: https://github.com/fszlin/certes/compare/v1.1.3...v1.1.4
[2.0.0]: https://github.com/fszlin/certes/compare/v1.1.4...v2.0.0

[i5]: https://github.com/fszlin/certes/issues/5
[i22]: https://github.com/fszlin/certes/issues/22
[i76]: https://github.com/fszlin/certes/issues/76
57 changes: 46 additions & 11 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ Certes is an [ACME](https://en.wikipedia.org/wiki/Automated_Certificate_Manageme
client runs on .NET 4.5+ and .NET Standard 1.3+, supports ACME v2 and wildcard certificates.
It is aimed to provide an easy to use API for managing certificates during deployment processes.

**Util Let's Encrypt releases [v2 endpoint](https://community.letsencrypt.org/t/acmev2-and-wildcard-launch-delay/53654),
please continue to use [v1 API](https://github.com/fszlin/certes/blob/master/docs/README.v1.md) for production.**

## Usage

Install [Certes](https://www.nuget.org/packages/Certes/) nuget package into your project:
```
```PowerShell
Install-Package Certes
```
or using .NET CLI:
```
```Batchfile
dotnet add package Certes
```

Expand All @@ -24,7 +21,22 @@ var acme = new AcmeContext(WellKnownServers.LetsEncryptStagingV2);
var account = acme.NewAccount("[email protected]", true);
```

Place an order for certificate
Place a wildcard certificate order
*(DNS validation is required for wildcard certificates)*
```C#
var order = await acme.NewOrder(new[] { "*.your.domain.name" });
```

Generate the value for DNS TXT record
```C#
var authz = (await order.Authorizations()).First();
var dnsChallenge = await authz.Dns();
var dnsTxt = acme.AccountKey.DnsTxt(dnsChallenge.Token);
```
Add a DNS TXT record to `_acme-challenge.your.domain.name`
with `dnsTxt` value.

For non-wildcard certificate, HTTP challenge is also available
```C#
var order = await acme.NewOrder(new[] { "your.domain.name" });
```
Expand All @@ -36,12 +48,12 @@ var httpChallenge = await authz.Http();
var keyAuthz = httpChallenge.KeyAuthz;
```

Prepare for http challenge by saving the **key authorization string**
in a text file, and upload it to `http://your.domain.name/.well-known/acme-challenge/<token>`
Save the **key authorization string** in a text file,
and upload it to `http://your.domain.name/.well-known/acme-challenge/<token>`

Ask the ACME server to validate our domain ownership
```C#
await httpChallenge.Validate();
await challenge.Validate();
```

Download the certificate once validation is done
Expand All @@ -68,18 +80,41 @@ Check the [APIs](APIv2.md) for more details.

*For ACME v1, please see [the doc here](README.v1.md).*

## CLI

The CLI is available as a dotnet global tool.
.NET Core Runtime 2.1+ *(currently in [preview](https://www.microsoft.com/net/download/dotnet-core/runtime-2.1.0-preview1))*
is required to use dotnet tools.

To install Certes CLI *(you may need to restart the console session if this is the first dotnet tool installed)*
```Batchfile
dotnet install tool --global dotnet-certes --version 1.0.1-master-812
```

Use the `--help` option to get started
```Batchfile
certes --help
```

or check this [AppVeyor script][AppVeyorCliSample] for renewing certificate on Azure webapps.

## Versioning

We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/fszlin/certes/tags).
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags](https://github.com/fszlin/certes/tags) on this repository.

Also check the [changelog](CHANGELOG.md) to see what's we are working on.

## CI Status
[![NuGet](https://img.shields.io/nuget/vpre/certes.svg)](https://www.nuget.org/packages/certes/absoluteLatest/)
[![NuGet](https://img.shields.io/nuget/vpre/certes.svg?label=Certes)](https://www.nuget.org/packages/certes/absoluteLatest/)
[![NuGet](https://img.shields.io/nuget/dt/certes.svg)](https://www.nuget.org/packages/certes/)
[![NuGet](https://img.shields.io/nuget/vpre/dotnet-certes.svg?label=CLI)](https://www.nuget.org/packages/dotnet-certes/absoluteLatest/)
[![NuGet](https://img.shields.io/nuget/dt/dotnet-certes.svg)](https://www.nuget.org/packages/dotnet-certes/)


[![AppVeyor](https://img.shields.io/appveyor/ci/fszlin/certes/master.svg)](https://ci.appveyor.com/project/fszlin/certes)
[![AppVeyor](https://img.shields.io/appveyor/tests/fszlin/certes/master.svg)](https://ci.appveyor.com/project/fszlin/certes/build/tests)
[![codecov](https://codecov.io/gh/fszlin/certes/branch/master/graph/badge.svg)](https://codecov.io/gh/fszlin/certes)
[![BCH compliance](https://bettercodehub.com/edge/badge/fszlin/certes?branch=master)](https://bettercodehub.com/results/fszlin/certes)

[tw]: https://twitter.com/share?url=https%3A%2F%2Fgithub.com%2Ffszlin%2Fcertes&via=certes_acme&related=fszlin&hashtags=certes%2Cssl%2Clets-encrypt%2Cacme%2Chttps&text=get%20free%20SSL%20via%20certes
[AppVeyorCliSample]: https://github.com/fszlin/lo0.in/blob/79fc1561ca4aa29de7741ad5590e53be8db34690/.appveyor.yml#L43-L56
2 changes: 1 addition & 1 deletion src/Certes.Cli/Certes.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp1.0;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks Condition="'$(CertesVersion)' != ''">netcoreapp2.0</TargetFrameworks>
<AssemblyVersion>1.0.0</AssemblyVersion>
<AssemblyVersion>1.0.1</AssemblyVersion>
<Version>$(AssemblyVersion)$(CertesPackageVersionSuffix)</Version>
<FileVersion>$(AssemblyVersion)$(CertesFileVersionSuffix)</FileVersion>
<InformationalVersion>$(AssemblyVersion)$(CertesInformationalVersionSuffix)</InformationalVersion>
Expand Down
69 changes: 55 additions & 14 deletions src/Certes.Cli/Commands/AzureAppCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.CommandLine;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Certes.Cli.Settings;
using Certes.Pkcs;
using Microsoft.Azure.Management.AppService.Fluent;
using Microsoft.Azure.Management.AppService.Fluent.Models;

Expand Down Expand Up @@ -70,29 +73,24 @@ public async Task<object> Execute(ArgumentSyntax syntax)
}

var cert = await orderCtx.Download();

var pfxName = $"{order.Certificate} by certes";
var pfxPassword = Guid.NewGuid().ToString("N");
var pfx = cert.ToPfx(privKey);
var pfxBytes = pfx.Build(pfxName, pfxPassword);

var certData = new CertificateInner
{
PfxBlob = pfxBytes,
Password = pfxPassword,
};
var x509Cert = new X509Certificate2(cert.Certificate.ToDer());
var thumbprint = x509Cert.Thumbprint;

using (var client = clientFactory.Invoke(azureCredentials))
{
client.SubscriptionId = azureCredentials.DefaultSubscriptionId;

var certUpdated = await client.Certificates.CreateOrUpdateAsync(
resourceGroup, pfxName, certData);
var certUploaded = await FindCertificate(client, resourceGroup, thumbprint);
if (certUploaded == null)
{
certUploaded = await UploadCertificate(
client, resourceGroup, appName, appSlot, cert.ToPfx(privKey), thumbprint);
}

var hostNameBinding = new HostNameBindingInner
{
SslState = SslState.SniEnabled,
Thumbprint = certUpdated.Thumbprint,
Thumbprint = certUploaded.Thumbprint,
};

var hostName = string.IsNullOrWhiteSpace(appSlot) ?
Expand All @@ -107,5 +105,48 @@ await client.WebApps.CreateOrUpdateHostNameBindingSlotAsync(
};
}
}

private static async Task<CertificateInner> UploadCertificate(
IWebSiteManagementClient client, string resourceGroup, string appName, string appSlot, PfxBuilder pfx, string thumbprint)
{
var pfxName = string.Format(CultureInfo.InvariantCulture, "[certes] {0:yyyyMMddhhmmss}", DateTime.UtcNow);
var pfxPassword = Guid.NewGuid().ToString("N");
var pfxBytes = pfx.Build(pfxName, pfxPassword);

var webApp = string.IsNullOrWhiteSpace(appSlot) ?
await client.WebApps.GetAsync(resourceGroup, appName) :
await client.WebApps.GetSlotAsync(resourceGroup, appName, appSlot);

var certData = new CertificateInner
{
PfxBlob = pfxBytes,
Password = pfxPassword,
Location = webApp.Location,
};

return await client.Certificates.CreateOrUpdateAsync(
resourceGroup, thumbprint, certData);
}

private static async Task<CertificateInner> FindCertificate(
IWebSiteManagementClient client, string resourceGroup, string thumbprint)
{
var certificates = await client.Certificates.ListByResourceGroupAsync(resourceGroup);
while (certificates != null)
{
foreach (var azCert in certificates)
{
if (string.Equals(azCert.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
{
return azCert;
}
}

certificates = certificates.NextPageLink == null ? null :
await client.Certificates.ListByResourceGroupNextAsync(certificates.NextPageLink);
}

return null;
}
}
}
2 changes: 1 addition & 1 deletion src/Certes.Cli/Commands/CertificatePemCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public async Task<object> Execute(ArgumentSyntax syntax)

return new
{
location = location,
location,
};

}
Expand Down
7 changes: 5 additions & 2 deletions src/Certes.Cli/Commands/CertificatePfxCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.CommandLine;
using System;
using System.CommandLine;
using System.Globalization;
using System.Threading.Tasks;
using Certes.Cli.Settings;
using NLog;
Expand Down Expand Up @@ -46,8 +48,9 @@ public async Task<object> Execute(ArgumentSyntax syntax)
var pwd = syntax.GetParameter<string>(PasswordParam, true);
var (location, cert) = await DownloadCertificate(syntax);

var pfxName = string.Format(CultureInfo.InvariantCulture, "[certes] {0:yyyyMMddhhmmss}", DateTime.UtcNow);
var privKey = await syntax.ReadKey(PrivateKeyOption, "CERTES_CERT_KEY", File, environment, true);
var pfx = cert.ToPfx(privKey).Build($"{location} by certes", pwd);
var pfx = cert.ToPfx(privKey).Build(pfxName, pwd);

var outPath = syntax.GetOption<string>(OutOption);
if (string.IsNullOrWhiteSpace(outPath))
Expand Down
2 changes: 2 additions & 0 deletions src/Certes/Acme/AcmeHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public async Task<AcmeHttpResponse<T>> Post<T>(Uri uri, object payload)
{
var payloadJson = JsonConvert.SerializeObject(payload, Formatting.None, jsonSettings);
var content = new StringContent(payloadJson, Encoding.UTF8, MimeJoseJson);
// boulder will reject the request if sending charset=utf-8
content.Headers.ContentType.CharSet = null;
using (var response = await Http.PostAsync(uri, content))
{
return await ProcessResponse<T>(response);
Expand Down
12 changes: 5 additions & 7 deletions src/Certes/Acme/ChallengeContext.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System;
using System.Threading.Tasks;
using Certes.Acme.Resource;

namespace Certes.Acme
{
/// <summary>
/// Represents the context for ACME challenge operations.
/// </summary>
/// <seealso cref="Certes.Acme.IChallengeContext" />
internal class ChallengeContext : EntityContext<Resource.Challenge>, IChallengeContext
internal class ChallengeContext : EntityContext<Challenge>, IChallengeContext
{
/// <summary>
/// Initializes a new instance of the <see cref="ChallengeContext"/> class.
Expand Down Expand Up @@ -57,13 +58,10 @@ public ChallengeContext(
/// <returns>
/// The challenge.
/// </returns>
public async Task<Resource.Challenge> Validate()
public async Task<Challenge> Validate()
{
var payload = await Context.Sign(
new Resource.Challenge {
KeyAuthorization = KeyAuthz
}, Location);
var resp = await Context.HttpClient.Post<Resource.Challenge>(Location, payload, true);
var payload = await Context.Sign(new { }, Location);
var resp = await Context.HttpClient.Post<Challenge>(Location, payload, true);
return resp.Resource;
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/Certes/Acme/Resource/AuthorizationStatus.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Certes.Acme.Resource
{
Expand All @@ -19,6 +20,7 @@ public enum AuthorizationStatus
/// <summary>
/// The processing status.
/// </summary>
[Obsolete("Use ChallengeStatus.Processing instead.")]
[EnumMember(Value = "processing")]
Processing,

Expand Down
6 changes: 6 additions & 0 deletions src/Certes/Acme/Resource/ChallengeStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public enum ChallengeStatus
[JsonProperty("pending")]
Pending,

/// <summary>
/// The processing status.
/// </summary>
[JsonProperty("processing")]
Processing,

/// <summary>
/// The valid status.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Certes/Acme/Resource/OrderStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public enum OrderStatus
[EnumMember(Value = "pending")]
Pending,

/// <summary>
/// The ready status.
/// </summary>
[EnumMember(Value = "ready")]
Ready,

/// <summary>
/// The processing status.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Certes/Certes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard1.3;net45;net47</TargetFrameworks>
<AssemblyVersion>2.0.0</AssemblyVersion>
<AssemblyVersion>2.0.1</AssemblyVersion>
<Version>$(AssemblyVersion)$(CertesPackageVersionSuffix)</Version>
<FileVersion>$(AssemblyVersion)$(CertesFileVersionSuffix)</FileVersion>
<InformationalVersion>$(AssemblyVersion)$(CertesInformationalVersionSuffix)</InformationalVersion>
Expand Down
2 changes: 1 addition & 1 deletion test/Certes.Tests.Web/Certes.Tests.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.2" />
<PackageReference Include="Microsoft.Azure.Management.Dns.Fluent" Version="1.7.0" />
</ItemGroup>
<ItemGroup>
Expand Down
Loading

0 comments on commit 4a70642

Please sign in to comment.