From bea13a36c8d82788c9f2a0c3ddce535ae53305f8 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 9 Dec 2018 20:15:26 -0700 Subject: [PATCH] Initial commit --- .github/CONTRIBUTING.md | 131 ++++++++++ .gitignore | 1 + LICENSE.txt | 201 ++++++++++++++++ README.md | 465 ++++++++++++++++++++++++++++++++++++ cache.go | 170 +++++++++++++ certificates.go | 352 +++++++++++++++++++++++++++ certmagic.go | 511 ++++++++++++++++++++++++++++++++++++++++ client.go | 425 +++++++++++++++++++++++++++++++++ config.go | 364 ++++++++++++++++++++++++++++ crypto.go | 155 ++++++++++++ filestorage.go | 126 ++++++++++ filestoragesync.go | 146 ++++++++++++ handshake.go | 401 +++++++++++++++++++++++++++++++ httphandler.go | 111 +++++++++ maintain.go | 305 ++++++++++++++++++++++++ memorysync.go | 85 +++++++ ocsp.go | 209 ++++++++++++++++ solvers.go | 147 ++++++++++++ storage.go | 212 +++++++++++++++++ user.go | 263 +++++++++++++++++++++ 20 files changed, 4780 insertions(+) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 cache.go create mode 100644 certificates.go create mode 100644 certmagic.go create mode 100644 client.go create mode 100644 config.go create mode 100644 crypto.go create mode 100644 filestorage.go create mode 100644 filestoragesync.go create mode 100644 handshake.go create mode 100644 httphandler.go create mode 100644 maintain.go create mode 100644 memorysync.go create mode 100644 ocsp.go create mode 100644 solvers.go create mode 100644 storage.go create mode 100644 user.go diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..24586b2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,131 @@ +Contributing to CertMagic +========================= + +## Common Tasks + +- [Contributing code](#contributing-code) +- [Reporting a bug](#reporting-bugs) +- [Suggesting an enhancement or a new feature](#suggesting-features) +- [Improving documentation](#improving-documentation) + +Other menu items: + +- [Values](#values) +- [Thank You](#thank-you) + + +### Contributing code + +You can have a direct impact on the project by helping with its code. To contribute code to CertMagic, open a [pull request](https://github.com/mholt/certmagic/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with CertMagic's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/mholt/certmagic). + +We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :wink: If your change is on the right track, we can guide you to make it mergable. + +Here are some of the expectations we have of contributors: + +- If your change is more than just a minor alteration, **open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that changes are in-line with the project's goals and the best interests of its users. If there's already an issue about it, comment on the existing issue to claim it. + +- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we DON'T do.](https://twitter.com/iamdevloper/status/397664295875805184) + +- [**Don't "push" your pull requests.**](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) Basically, work with—not against—the maintainers -- theirs is not a glorious job. In fact, consider becoming a CertMagic maintainer yourself! + +- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other. + +- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass. + +- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling. + +- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft ` then `git commit -s`. + +- **Maintain your contributions.** Please help maintain your change after it is merged. + +- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious, and comments should not state the obvious. + +We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base! + + +### HOW TO MAKE A PULL REQUEST TO CERTMAGIC + +Contributing to Go projects on GitHub is fun and easy. We recommend the following workflow: + +1. [Fork this repo](https://github.com/mholt/certmagic). This makes a copy of the code you can write to. + +2. If you don't already have this repo (mholt/certmagic.git) repo on your computer, get it with `go get github.com/mholt/certmagic`. + +3. Tell git that it can push the mholt/certmagic.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/certmagic.git` + +4. Make your changes in the mholt/certmagic.git repo on your computer. + +5. Push your changes to your fork: `git push myfork` + +6. [Create a pull request](https://github.com/mholt/certmagic/pull/new/master) to merge your changes into mholt/certmagic @ master. (Click "compare across forks" and change the head fork.) + +This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want. + + + +### Reporting bugs + +Like every software, CertMagic has its flaws. If you find one, [search the issues](https://github.com/mholt/certmagic/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/mholt/certmagic/issues/new) and describe the bug clearly. + +**You can help stop bugs in their tracks!** Speed up the patching process by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too. + +Please follow the issue template so we have all the needed information. Unredacted—yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. The burden is on you to convince us that it is actually a bug in CertMagic. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you! + +Failure to fill out the issue template will probably result in the issue being closed. + +We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html). + +Please be kind. :smile: Remember that CertMagic comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else! + + +### Suggesting features + +First, [search to see if your feature has already been requested](https://github.com/mholt/certmagic/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. You don't have to follow the bug template for feature requests. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and without clarification will have to be closed. + +**Please do not "bump" issues with comments that ask if there are any updates.** + +While we really do value your requests and implement many of them, not all features are a good fit for CertMagic. If a feature is not in the best interest of the CertMagic project or its users in general, we may politely decline to implement it. + + + +## Collaborator Instructions + +Collabators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help.The expectations we have of collaborators are: + +- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider: + - Can the change be made more elegant? + - Is this a maintenance burden? + - What assumptions does the code make? + - Is it well-tested? + - Is the change a good fit for the project? + - Does it actually fix the problem or is it creating a special case instead? + - Does the change incur any new dependencies? (Avoid these!) + +- **Answer issues.** If every collaborator helped out with issues, we could count the number of open issues on two hands. This means getting involved in the discussion, investigating the code, and yes, debugging it. It's fun. Really! :smile: Please, please help with open issues. Granted, some issues need to be done before others. And of course some are larger than others: you don't have to do it all yourself. Work with other collaborators as a team! + +- **Do not merge pull requests until they have been approved by one or two other collaborators.** If a project owner approves the PR, it can be merged (as long as the conversation has finished too). + +- **Prefer squashed commits over a messy merge.** If there are many little commits, please [squash the commits](https://stackoverflow.com/a/11732910/1048862) so we don't clutter the commit history. + +- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. + +- **Make sure tests test the actual thing.** Double-check that the tests fail without the change, and pass with it. It's important that they assert what they're purported to assert. + +- **Recommended reading** + - [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) for an idea of what we look for in good, clean Go code + - [Linus Torvalds describes a good commit message](https://gist.github.com/matthewhudson/1475276) + - [Best Practices for Maintainers](https://opensource.guide/best-practices/) + - [Shrinking Code Review](https://alexgaynor.net/2015/dec/29/shrinking-code-review/) + + + +## Values + +- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate. + +- The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good. + + +## Thank you + +Thanks for your help! CertMagic would not be what it is today without your contributions. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbd281d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_gitignore/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..89297c9 --- /dev/null +++ b/README.md @@ -0,0 +1,465 @@ +

+ CertMagic +

+

Easy and Powerful TLS Automation

+

The same library used by the Caddy Web Server

+

+ + + + certmagic on Sourcegraph +

+ + +Caddy's automagic TLS features, now for your own Go programs, in one powerful and easy-to-use library! + +CertMagic is the most mature, robust, and capable ACME client integration for Go. + +With CertMagic, you can add one line to your Go application to serve securely over TLS, without ever having to touch certificates. + +Instead of: + +```go +// plaintext HTTP, gross 🤢 +http.ListenAndServe(":80", mux) +``` + +Use CertMagic: + +```go +// encrypted HTTPS with HTTP->HTTPS redirects - yay! 🔒😍 +certmagic.HTTPS("example.com", mux) +``` + +That line of code will serve your HTTP router `mux` over HTTPS, complete with HTTP->HTTPS redirects. It obtains and renews the TLS certificates. It staples OCSP responses for greater privacy and security. As long as your domain name points to your server, CertMagic will keep its connections secure. + +Compared to other ACME client libraries for Go, only CertMagic supports the full suite of ACME features, and no other library will ever match CertMagic's maturity and reliability. + + + + +CertMagic - Automatic HTTPS using Let's Encrypt +=============================================== + +**Sponsored by Relica - Cross-platform local and cloud file backup:** + +Relica - Cross-platform file backup to the cloud, local disks, or other computers + + +## Menu + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) + - [Package Overview](#package-overview) + - [Certificate authority](#certificate-authority) + - [The `Config` type](#the-config-type) + - [Defaults](#defaults) + - [Providing an email address](#providing-an-email-address) + - [Development and testing](#development-and-testing) + - [Examples](#examples) + - [Serving HTTP handlers with HTTPS](#serving-http-handlers-with-https) + - [Starting a TLS listener](#starting-a-tls-listener) + - [Getting a tls.Config](#getting-a-tls-config) + - [Advanced use](#advanced-use) + - [Wildcard Certificates](#wildcard-certificates) + - [Behind a load balancer (or in a cluster)](#behind-a-load-balancer) + - [The ACME Challenges](#the-acme-challenges) + - [HTTP Challenge](#http-challenge) + - [TLS-ALPN Challenge](#tls-alpn-challenge) + - [DNS Challenge](#dns-challenge) + - [On-Demand TLS](#on-demand-tls) + - [Storage](#storage) + - [Cache](#cache) +- [Contributing](#contributing) +- [Project History](#project-history) +- [Credits and License](#credits-and-license) + + +## Features + +- Fully automated certificate management including issuance and renewal +- One-liner, fully managed HTTPS servers +- Full control over almost every aspect of the system +- HTTP->HTTPS redirects (for HTTP applications) +- Solves all 3 ACME challenges: HTTP, TLS-ALPN, and DNS +- Over 50 DNS providers work out-of-the-box (powered by [lego](https://github.com/xenolf/lego)!) +- Pluggable storage implementations (default: file system) +- Wildcard certificates (requires DNS challenge) +- OCSP stapling for each qualifying certificate ([done right](https://gist.github.com/sleevi/5efe9ef98961ecfb4da8#gistcomment-2336055)) +- Distributed solving of all challenges (works behind load balancers) +- Supports "on-demand" issuance of certificates (during TLS handshakes!) + - Custom decision functions + - Hostname whitelist + - Ask an external URL + - Rate limiting +- Optional event hooks to observe internal behaviors +- Works with any certificate authority (CA) compliant with the ACME specification +- Certificate revocation (please, only if private key is compromised) +- Must-Staple (optional; not default) +- Cross-platform support! Mac, Windows, Linux, BSD, Android... +- Scales well to thousands of names/certificates per instance +- Use in conjunction with your own certificates + + +## Requirements + +1. Public DNS name(s) you control +2. Server reachable from public Internet + - Or use the DNS challenge to waive this requirement +3. Control over port 80 (HTTP) and/or 443 (HTTPS) + - Or they can be forwarded to other ports you control + - Or use the DNS challenge to waive this requirement + - (This is a requirement of the ACME protocol, not a library limitation) +4. Persistent storage + - Typically the local file system (default) + - Other integrations available/possible + +**_Before using this library, your domain names MUST be pointed (A/AAAA records) at your server (unless you use the DNS challenge)!_** + + +## Installation + +```bash +$ go get -u github.com/mholt/certmagic +``` + + +## Usage + +### Package Overview + +#### Certificate authority + +This library uses Let's Encrypt by default, but you can use any certificate authority that conforms to the ACME specification. Known/common CAs are provided as consts in the package, for example `LetsEncryptStagingCA` and `LetsEncryptProductionCA`. + +#### The `Config` type + +The `certmagic.Config` struct is how you can wield the power of this fully armed and operational battle station. However, an empty config is _not_ a valid one! In time, you will learn to use the force of `certmagic.New(certmagic.Config{...})` as I have. + +#### Defaults + +For every field in the `Config` struct, there is a corresponding package-level variable you can set as a default value. These defaults will be used when you call any of the high-level convenience functions like `HTTPS()` or `Listen()` or anywhere else a default `Config` is used. They are also used for any `Config` fields that are zero-valued when you call `New()`. + +You can set these values easily, for example: `certmagic.Email = ...` sets the email address to use for everything unless you explicitly override it in a Config. + + + +#### Providing an email address + +Although not strictly required, this is highly recommended best practice. It allows you to receive expiration emails if your certificates are expiring for some reason, and also allows the CA's engineers to potentially get in touch with you if something is wrong. I recommend setting `certmagic.Email` or always setting the `Email` field of the `Config` struct. + + +### Development and Testing + +Note that Let's Encrypt imposes [strict rate limits](https://letsencrypt.org/docs/rate-limits/) at its production endpoint, so using it while developing your application may lock you out for a few days if you aren't careful! + +While developing your application and testing it, use [their staging endpoint](https://letsencrypt.org/docs/staging-environment/) which has much higher rate limits. Even then, don't hammer it: but it's much safer for when you're testing. When deploying, though, use their production CA because their staging CA doesn't issue trusted certificates. + +To use staging, set `certmagic.CA = certmagic.LetsEncryptStagingCA` or set `CA` of every `Config` struct. + + + +### Examples + +There are many ways to use this library. We'll start with the highest-level (simplest) and work down (more control). + +First, we'll follow best practices and do the following: + +```go +// read and agree to your CA's legal documents +certmagic.Agreed = true + +// provide an email address +certmagic.Email = "you@yours.com" + +// use the staging endpoint while we're developing +certmagic.CA = certmagic.LetsEncryptStagingCA +``` + + +#### Serving HTTP handlers with HTTPS + +```go +err := certmagic.HTTPS([]string{"example.com", "www.example.com"}, mux) +if err != nil { + return err +} +``` + +#### Starting a TLS listener + +```go +ln, err := certmagic.Listen([]string{"example.com"}) +if err != nil { + return err +} +``` + + +#### Getting a tls.Config + +```go +tlsConfig, err := certmagic.TLS([]string{"example.com"}) +if err != nil { + return err +} +``` + + +#### Advanced use + +For more control, you'll make and use a `Config` like so: + +```go +magic := certmagic.New(certmagic.Config{ + CA: certmagic.LetsEncryptStagingCA, + Email: "you@yours.com", + Agreed: true, + // any other customization you want +}) + +// this obtains certificates or renews them if necessary +err := magic.Manage([]string{"example.com", "sub.example.com"}) +if err != nil { + return err +} + +// to use its certificates and solve the TLS-ALPN challenge, +// you can get a TLS config to use in a TLS listener! +tlsConfig := magic.TLSConfig() + +// if you already have a TLS config you don't want to replace, +// we can simply set its GetCertificate field and append the +// TLS-ALPN challenge protocol to the NextProtos +myTLSConfig.GetCertificate = magic.GetCertificate +myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, acme.ACMETLS1Protocol} + +// the HTTP challenge has to be handled by your HTTP server; +// if you don't have one, you should have disabled it earlier +// when you made the certmagic.Config +httpMux = magic.HTTPChallengeHandler(mux) +``` + +Great! This example grants you much more flexibility for advanced programs. However, _the vast majority of you will only use the high-level functions described earlier_, especially since you can still customize them by setting the package-level defaults. + +If you want to use the default configuration but you still need a `certmagic.Config`, you can call `certmagic.Manage()` directly to get one: + +```go +magic, err := certmagic.Manage([]string{"example.com"}) +if err != nil { + return err +} +``` + +And then it's the same as above, as if you had made the `Config` yourself. + + +### Wildcard certificates + +At time of writing (December 2018), Let's Encrypt only issues wildcard certificates with the DNS challenge. + + +### Behind a load balancer (or in a cluster) + +CertMagic runs effectively behind load balancers and/or in cluster/fleet environments. In other words, you can have 10 or 1,000 servers all serving the same domain names, all sharing certificates and OCSP staples. + +To do so, simply ensure that each instance is using the same Storage and Sync. That is the sole criteria for determining whether an instance is part of a cluster. + +The default Storage and Sync are implemented using the file system, so mounting the same shared folder is sufficient! (See [Storage](#storage) for more on that.) If you need an alternate Storage or Sync implementation, feel free to use one, provided that all the instances use the _same_ one. :) + +Although Storage and Sync seem closely related, they are, in fact, distinct concepts, although in some cases a Locker may rely on a Storage (such as the default FileSystem ones). Storage is where assets are stored, whereas Sync is what coordinates certificate-related tasks. Storage keeps certificates that are obtained, whereas Sync ensures that a certificate is obtained only once at a time. + +See [Storage](#storage) and the associated [godoc](TODO) for more information! + +## The ACME Challenges + +This section describes how to solve the ACME challenges. + +If you're using the high-level convenience functions like `HTTPS()`, `Listen()`, or `TLS()`, the HTTP and/or TLS-ALPN challenges are solved for you because they also start listeners. However, if you're making a `Config` and you start your own server manually, you'll need to be sure the ACME challenges can be solved so certificates can be renewed. + +The HTTP and TLS-ALPN challenges are the defaults because they don't require configuration from you, but they require that your server is accessible from external IPs on low ports. If that is not possible in your situation, you can enable the DNS challenge, which will disable the HTTP and TLS-ALPN challenges and use the DNS challenge exclusively. + +Only one challenge technically needs to be enabled for things to work, but using multiple is good for reliability in case a challenge is discontinued by the CA. This happened to the TLS-SNI challenge in early 2018—many popular ACME clients such as Traefik and Autocert broke, resulting in downtime for some sites, until new releases were made and patches deployed, because they used only one challenge; Caddy, however—this library's forerunner—was unaffected because it also used the HTTP challenge. If multiple challenges are enabled, they are chosen randomly to help prevent false reliance on a single challenge type. + + +### HTTP Challenge + +Per the ACME spec, the HTTP challenge requires port 80, or at least packet forwarding from port 80. It works by serving a specific HTTP response that only the genuine server would have to a normal HTTP request at a special endpoint. + +If you are running an HTTP server, solving this challenge is very easy: just wrap your handler in `HTTPChallengeHandler` _or_ call `SolveHTTPChallenge()` inside your own `ServeHTTP()` method. + +For example, if you're using the standard library: + +```go +mux := http.NewServeMux() +mux.Handle("/", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Lookit my cool website over HTTPS!") +}) + +http.ListenAndServe(":80", magic.HTTPChallengeHandler(mux)) +``` + +If wrapping your handler is not a good solution, try this inside your `ServeHTTP()` instead: + +```go +magic := certmagic.NewDefault() + +func ServeHTTP(w http.ResponseWriter, req *http.Request) { + if magic.HandleHTTPChallenge(w, r) { + return // challenge handled; nothing else to do + } + ... +} +``` + +If you are not running an HTTP server, you should disable the HTTP challenge _or_ run an HTTP server whose sole job it is to solve the HTTP challenge. + + +### TLS-ALPN Challenge + +Per the ACME spec, the TLS-ALPN challenge requires port 443, or at least packet forwarding from port 443. It works by providing a special certificate using a standard TLS extension, Application Layer Protocol Negotiation (ALPN), having a special value. This is the most convenient challenge type because it usually requires no extra configuration and uses the standard TLS port which is where the certificates are used, also. + +This challenge is easy to solve: just use the provided `tls.Config` when you make your TLS listener: + +```go +// use this to configure a TLS listener +tlsConfig := magic.TLSConfig() +``` + +Or make two simple changes to an existing `tls.Config`: + +```go +myTLSConfig.GetCertificate = magic.GetCertificate +myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, acme.ACMETLS1Protocol} +``` + +Then just make sure your TLS listener is listening on port 443: + +```go +ln, err := tls.Listen("tcp", ":443", myTLSConfig) +``` + + +### DNS Challenge + +The DNS challenge is perhaps the most useful challenge because it allows you to obtain certificates without your server needing to be publicly accessible on the Internet, and it's the only challenge by which Let's Encrypt will issue wildcard certificates. + +This challenge works by setting a special record in the domain's zone. To do this automatically, your DNS provider needs to offer an API by which changes can be made to domain names, and the changes need to take effect immediately for best results. CertMagic supports [all of lego's DNS provider implementations](https://github.com/xenolf/lego/tree/master/providers/dns)! All of them clean up the temporary record after the challenge completes. + +To enable it, just set the `DNSProvider` field on a `certmagic.Config` struct, or set the default `certmagic.DNSProvider` variable. For example, if my domains' DNS was served by DNSimple (they're great, by the way) and I set my DNSimple API credentials in environment variables: + +```go +import "github.com/xenolf/lego/providers/dns/dnsimple" + +provider, err := dnsimple.NewProvider() +if err != nil { + return err +} + +certmagic.DNSProvider = provider +``` + +Then the DNS challenge will be used by default, and I can obtain certificates for wildcard domains now. See the godoc documentation for the provider you're using to learn how to configure it. Most can be configured by env variables or by passing in a config struct. + +Enabling the DNS challenge disables the other challenges for that `certmagic.Config` instance. + + +## On-Demand TLS + +Normally, certificates are obtained and renewed before a listener starts serving, and then those certificates are maintained throughout the lifetime of the program. In other words, the certificate names are static. But sometimes you don't know all the names ahead of time. This is where On-Demand TLS shines. + +Originally invented for use in Caddy (which was the first program to use such technology), On-Demand TLS makes it possible and easy to serve certificates for arbitrary names during the lifetime of the server. When a TLS handshake is received, CertMagic will read the Server Name Indication (SNI) value and either load and present that certificate in the ServerHello, or if one does not exist, it will obtain it from a CA right then-and-there. + +Of course, this has some obvious security implications. You don't want to DoS a CA or allow arbitrary clients to fill your storage with spammy TLS handshakes. That's why, in order to enable On-Demand issuance, you'll need to set some limits or some policy to allow getting a certificate. + +CertMagic provides several ways to enforce decision policies for On-Demand TLS, in descending order of priority: + +- A generic function that you write which will decide whether to allow the certificate request +- A name whitelist +- The ability to make an HTTP request to a URL for permission +- Rate limiting + +The simplest way to enable On-Demand issuance is to set the OnDemand field of a Config (or the default package-level value): + +```go +certmagic.OnDemand = &certmagic.OnDemandConfig{MaxObtain: 5} +``` + +This allows only 5 certificates to be requested and is the simplest way to enable On-Demand TLS, but is the least recommended. It prevents abuse, but only in the least helpful way. + +The [godoc](TODO) describes how to use the other policies, all of which are much-more recommended. + +If `OnDemand` is set and `Manage()` is called, then the names given to `Manage()` will be whitelisted rather than obtained right away. + + +## Storage + +CertMagic relies on storage to store certificates and other TLS assets (OCSP staple cache, coordinating locks, etc). Persistent storage is a requirement when using CertMagic: ephemeral storage will likely lead to rate limiting on the CA-side as CertMagic will always have to get new certificates. + +By default, CertMagic stores assets on the local file system in `$HOME/.local/share/certmagic` (and honors `$XDG_CACHE_HOME` if set). CertMagic will create the directory if it does not exist. If writes are denied, things will not be happy, so make sure CertMagic can write to it! + +The notion of a "cluster" or "fleet" of instances that may be serving the same site and sharing certificates, etc, is tied to storage. Simply, any instances that use the same storage facilities are considered part of the cluster. So if you deploy 100 instances of CertMagic behind a load balancer, they are all part of the same cluster if they share storage. Sharing storage could be mounting a shared folder, or implementing some other distributed storage such as a database server or KV store. + +The easiest way to change the storage being used is to set `certmagic.DefaultStorage` to a value that satisfies the [Storage interface](TODO). + +If you write a Storage or Sync implementation, let us know and we'll add it to the project so people can find it! + + +## Cache + +All of the certificates in use are de-duplicated and cached in memory for optimal performance at handshake-time. This cache must be backed by persistent storage as described above. + +Most applications will not need to interact with certificate caches directly. Usually, the closest you will come is to set the package-wide `certmagic.DefaultStorage` variable (before attempting to create any Configs). However, if your use case requires using different storage facilities for different Configs (that's highly unlikely and NOT recommended! Even Caddy doesn't get that crazy), you will need to call `certmagic.NewCache()` and pass in the storage you want to use, then get new `Config` structs with `certmagic.NewWithCache()` and pass in the cache. + +Again, if you're needing to do this, you've probably over-complicated your application design. + + +## FAQ + +### Can I use some of my own certificates while using CertMagic? + +Yes, just call the proper method on the `Config` to cache an "unmanaged certificate": (TODO: godoc links) + + +### Does CertMagic obtain SAN certificates? + +Technically all certificates these days are SAN certificates because CommonName is deprecated. But if you're asking whether CertMagic issues and manages certificates with multiple SANs, the answer is no. But it does support those, if you provide your own. + + + +## Contributing + +We welcome your contributions! Please see our **[contributing guidelines](https://github.com/mholt/certmagic/blob/master/.github/CONTRIBUTING.md)** for instructions. + + +## Project History + +CertMagic is the core of Caddy's advanced TLS automation code, extracted into a library. The underlying ACME client implementation is [lego](https://github.com/xenolf/lego), which was originally developed for use in Caddy even before Let's Encrypt entered public beta in 2015. + +In the years since then, Caddy's TLS automation techniques have been widely adopted, tried and tested in production, and served millions of sites and secured trillions of connections. + +Now, CertMagic is _the actual library used by Caddy_. It's incredibly powerful and feature-rich, but also easy to use for simple Go programs: one line of code can enable fully-automated HTTPS applications with HTTP->HTTPS redirects. + +Caddy is known for its robust HTTPS+ACME features. When ACME certificate authorities have had outages, in some cases Caddy was the only major client that didn't experience any downtime. Caddy can weather OCSP outages lasting days, or CA outages lasting weeks, without taking your sites offline. + +Caddy was also the first to sport "on-demand" issuance technology, which obtains certificates during the first TLS handshake for an allowed SNI name. + +Consequently, CertMagic brings all these (and more) features and capabilities right into your own Go programs. + + +## Credits and License + +CertMagic is a project by [Matthew Holt](https://twitter.com/mholt6), who is the author; and various contributors, who are credited in the commit history of either CertMagic or Caddy. + +CertMagic is licensed under Apache 2.0, an open source license. For convenience, its main points are summarized as follows (but this is no replacement for the actual license text): + +- The author owns the copyright to this code +- Use, distribute, and modify the software freely +- Private and internal use is allowed +- License text and copyright notices must stay intact and be included with distributions +- Any and all changes to the code must be documented diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..339e583 --- /dev/null +++ b/cache.go @@ -0,0 +1,170 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "fmt" + "sync" + "time" +) + +// Cache is a structure that stores certificates in memory. +// Generally, there should only be one per process. However, +// complex applications that virtualize the concept of a +// "process" (such as Caddy, which virtualizes processes as +// "instances" so it can do graceful, in-memory reloads of +// its configuration) may use more of these per OS process. +// +// Using just one cache per process avoids duplication of +// certificates across multiple configurations and makes +// maintenance easier. +// +// An empty cache is INVALID and must not be used. +// Be sure to call NewCertificateCache to get one. +// +// These should be very long-lived values, and must not be +// copied. Before all references leave scope to be garbage +// collected, ensure you call Stop() to stop maintenance +// maintenance on the certificates stored in this cache. +type Cache struct { + // How often to check certificates for renewal + RenewInterval time.Duration + + // How often to check if OCSP stapling needs updating + OCSPInterval time.Duration + + // The storage implementation + storage Storage + + // The cache is keyed by certificate hash + cache map[string]Certificate + + // Protects the cache map + mu sync.RWMutex + + // Close this channel to cancel asset maintenance + stopChan chan struct{} +} + +// NewCache returns a new, valid Cache backed by the +// given storage implementation. It also begins a +// maintenance goroutine for any managed certificates +// stored in this cache. +// +// See the godoc for Cache to use it properly. +// +// Note that all processes running in a cluster +// configuration must use the same storage value +// in order to share certificates. (A single storage +// value may be shared by multiple clusters as well.) +func NewCache(storage Storage) *Cache { + c := &Cache{ + RenewInterval: DefaultRenewInterval, + OCSPInterval: DefaultOCSPInterval, + storage: storage, + cache: make(map[string]Certificate), + stopChan: make(chan struct{}), + } + go c.maintainAssets() + return c +} + +// Stop stops the maintenance goroutine for +// certificates in certCache. +func (certCache *Cache) Stop() { + close(certCache.stopChan) +} + +// replaceCertificate replaces oldCert with newCert in the cache, and +// updates all configs that are pointing to the old certificate to +// point to the new one instead. newCert must already be loaded into +// the cache (this method does NOT load it into the cache). +// +// Note that all the names on the old certificate will be deleted +// from the name lookup maps of each config, then all the names on +// the new certificate will be added to the lookup maps as long as +// they do not overwrite any entries. +// +// The newCert may be modified and its cache entry updated. +// +// This method is safe for concurrent use. +func (certCache *Cache) replaceCertificate(oldCert, newCert Certificate) error { + certCache.mu.Lock() + defer certCache.mu.Unlock() + + // have all the configs that are pointing to the old + // certificate point to the new certificate instead + for _, cfg := range oldCert.configs { + // first delete all the name lookup entries that + // pointed to the old certificate + for name, certKey := range cfg.certificates { + if certKey == oldCert.Hash { + delete(cfg.certificates, name) + } + } + + // then add name lookup entries for the names + // on the new certificate, but don't overwrite + // entries that may already exist, not only as + // a courtesy, but importantly: because if we + // overwrote a value here, and this config no + // longer pointed to a certain certificate in + // the cache, that certificate's list of configs + // referring to it would be incorrect; so just + // insert entries, don't overwrite any + for _, name := range newCert.Names { + if _, ok := cfg.certificates[name]; !ok { + cfg.certificates[name] = newCert.Hash + } + } + } + + // since caching a new certificate attaches only the config + // that loaded it, the new certificate needs to be given the + // list of all the configs that use it, so copy the list + // over from the old certificate to the new certificate + // in the cache + newCert.configs = oldCert.configs + certCache.cache[newCert.Hash] = newCert + + // finally, delete the old certificate from the cache + delete(certCache.cache, oldCert.Hash) + + return nil +} + +// reloadManagedCertificate reloads the certificate corresponding to the name(s) +// on oldCert into the cache, from storage. This also replaces the old certificate +// with the new one, so that all configurations that used the old cert now point +// to the new cert. +func (certCache *Cache) reloadManagedCertificate(oldCert Certificate) error { + // get the certificate from storage and cache it + newCert, err := oldCert.configs[0].CacheManagedCertificate(oldCert.Names[0]) + if err != nil { + return fmt.Errorf("unable to reload certificate for %v into cache: %v", oldCert.Names, err) + } + + // and replace the old certificate with the new one + err = certCache.replaceCertificate(oldCert, newCert) + if err != nil { + return fmt.Errorf("replacing certificate %v: %v", oldCert.Names, err) + } + + return nil +} + +// defaultCache is a convenient, default certificate cache for +// use by this process when no other certificate cache is provided. +var defaultCache = NewCache(DefaultStorage) diff --git a/certificates.go b/certificates.go new file mode 100644 index 0000000..571dc5d --- /dev/null +++ b/certificates.go @@ -0,0 +1,352 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "strings" + "time" + + "golang.org/x/crypto/ocsp" +) + +// Certificate is a tls.Certificate with associated metadata tacked on. +// Even if the metadata can be obtained by parsing the certificate, +// we are more efficient by extracting the metadata onto this struct. +type Certificate struct { + tls.Certificate + + // Names is the list of names this certificate is written for. + // The first is the CommonName (if any), the rest are SAN. + Names []string + + // NotAfter is when the certificate expires. + NotAfter time.Time + + // OCSP contains the certificate's parsed OCSP response. + OCSP *ocsp.Response + + // The hex-encoded hash of this cert's chain's bytes. + Hash string + + // configs is the list of configs that use or refer to + // The first one is assumed to be the config that is + // "in charge" of this certificate (i.e. determines + // whether it is managed, how it is managed, etc). + // This field will be populated by cacheCertificate. + // Only meddle with it if you know what you're doing! + configs []*Config + + // whether this certificate is under our management + managed bool +} + +// NeedsRenewal returns true if the certificate is +// expiring soon or has expired. +func (c Certificate) NeedsRenewal() bool { + if c.NotAfter.IsZero() { + return false + } + timeLeft := c.NotAfter.UTC().Sub(time.Now().UTC()) + renewDurationBefore := DefaultRenewDurationBefore + if len(c.configs) > 0 && c.configs[0].RenewDurationBefore > 0 { + renewDurationBefore = c.configs[0].RenewDurationBefore + } + return timeLeft < renewDurationBefore +} + +// CacheManagedCertificate loads the certificate for domain into the +// cache, from the TLS storage for managed certificates. It returns a +// copy of the Certificate that was put into the cache. +// +// This method is safe for concurrent use. +func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) { + certRes, err := cfg.loadCertResource(domain) + if err != nil { + return Certificate{}, err + } + cert, err := cfg.makeCertificateWithOCSP(certRes.Certificate, certRes.PrivateKey) + if err != nil { + return cert, err + } + cert.managed = true + if cfg.OnEvent != nil { + cfg.OnEvent("cached_managed_cert", cert.Names) + } + return cfg.cacheCertificate(cert), nil +} + +// CacheUnmanagedCertificatePEMFile loads a certificate for host using certFile +// and keyFile, which must be in PEM format. It stores the certificate in +// the in-memory cache. +// +// This method is safe for concurrent use. +func (cfg *Config) CacheUnmanagedCertificatePEMFile(certFile, keyFile string) error { + cert, err := cfg.makeCertificateFromDiskWithOCSP(certFile, keyFile) + if err != nil { + return err + } + cfg.cacheCertificate(cert) + if cfg.OnEvent != nil { + cfg.OnEvent("cached_unmanaged_cert", cert.Names) + } + return nil +} + +// CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache. +// It staples OCSP if possible. +// +// This method is safe for concurrent use. +func (cfg *Config) CacheUnmanagedTLSCertificate(tlsCert tls.Certificate) error { + var cert Certificate + err := fillCertFromLeaf(&cert, tlsCert) + if err != nil { + return err + } + err = cfg.certCache.stapleOCSP(&cert, nil) + if err != nil { + log.Printf("[WARNING] Stapling OCSP: %v", err) + } + if cfg.OnEvent != nil { + cfg.OnEvent("cached_unmanaged_cert", cert.Names) + } + cfg.cacheCertificate(cert) + return nil +} + +// CacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes +// of the certificate and key, then caches it in memory. +// +// This method is safe for concurrent use. +func (cfg *Config) CacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error { + cert, err := cfg.makeCertificateWithOCSP(certBytes, keyBytes) + if err != nil { + return err + } + cfg.cacheCertificate(cert) + if cfg.OnEvent != nil { + cfg.OnEvent("cached_unmanaged_cert", cert.Names) + } + return nil +} + +// makeCertificateFromDiskWithOCSP makes a Certificate by loading the +// certificate and key files. It fills out all the fields in +// the certificate except for the Managed and OnDemand flags. +// (It is up to the caller to set those.) It staples OCSP. +func (cfg *Config) makeCertificateFromDiskWithOCSP(certFile, keyFile string) (Certificate, error) { + certPEMBlock, err := ioutil.ReadFile(certFile) + if err != nil { + return Certificate{}, err + } + keyPEMBlock, err := ioutil.ReadFile(keyFile) + if err != nil { + return Certificate{}, err + } + return cfg.makeCertificateWithOCSP(certPEMBlock, keyPEMBlock) +} + +// makeCertificate turns a certificate PEM bundle and a key PEM block into +// a Certificate with necessary metadata from parsing its bytes filled into +// its struct fields for convenience (except for the OnDemand and Managed +// flags; it is up to the caller to set those properties!). This function +// does NOT staple OCSP. +func (*Config) makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { + var cert Certificate + + // Convert to a tls.Certificate + tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return cert, err + } + + // Extract necessary metadata + err = fillCertFromLeaf(&cert, tlsCert) + if err != nil { + return cert, err + } + + return cert, nil +} + +// makeCertificateWithOCSP is the same as makeCertificate except that it also +// staples OCSP to the certificate. +func (cfg *Config) makeCertificateWithOCSP(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { + cert, err := cfg.makeCertificate(certPEMBlock, keyPEMBlock) + if err != nil { + return cert, err + } + err = cfg.certCache.stapleOCSP(&cert, certPEMBlock) + if err != nil { + log.Printf("[WARNING] Stapling OCSP: %v", err) + } + return cert, nil +} + +// fillCertFromLeaf populates metadata fields on cert from tlsCert. +func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error { + if len(tlsCert.Certificate) == 0 { + return errors.New("certificate is empty") + } + cert.Certificate = tlsCert + + // the leaf cert should be the one for the site; it has what we need + leaf, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + + if leaf.Subject.CommonName != "" { // TODO: CommonName is deprecated + cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)} + } + for _, name := range leaf.DNSNames { + if name != leaf.Subject.CommonName { // TODO: CommonName is deprecated + cert.Names = append(cert.Names, strings.ToLower(name)) + } + } + for _, ip := range leaf.IPAddresses { + if ipStr := ip.String(); ipStr != leaf.Subject.CommonName { // TODO: CommonName is deprecated + cert.Names = append(cert.Names, strings.ToLower(ipStr)) + } + } + for _, email := range leaf.EmailAddresses { + if email != leaf.Subject.CommonName { // TODO: CommonName is deprecated + cert.Names = append(cert.Names, strings.ToLower(email)) + } + } + if len(cert.Names) == 0 { + return errors.New("certificate has no names") + } + + // save the hash of this certificate (chain) and + // expiration date, for necessity and efficiency + cert.Hash = hashCertificateChain(cert.Certificate.Certificate) + cert.NotAfter = leaf.NotAfter + + return nil +} + +// managedCertInStorageExpiresSoon returns true if cert (being a +// managed certificate) is expiring within RenewDurationBefore. +// It returns false if there was an error checking the expiration +// of the certificate as found in storage, or if the certificate +// in storage is NOT expiring soon. A certificate that is expiring +// soon in our cache but is not expiring soon in storage probably +// means that another instance renewed the certificate in the +// meantime, and it would be a good idea to simply load the cert +// into our cache rather than repeating the renewal process again. +func managedCertInStorageExpiresSoon(cert Certificate) (bool, error) { + if len(cert.configs) == 0 { + return false, fmt.Errorf("no configs for certificate") + } + cfg := cert.configs[0] + certRes, err := cfg.loadCertResource(cert.Names[0]) + if err != nil { + return false, err + } + tlsCert, err := tls.X509KeyPair(certRes.Certificate, certRes.PrivateKey) + if err != nil { + return false, err + } + leaf, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return false, err + } + timeLeft := leaf.NotAfter.Sub(time.Now().UTC()) + return timeLeft < cfg.RenewDurationBefore, nil +} + +// cacheCertificate adds cert to the in-memory cache. If a certificate +// with the same hash is already cached, it is NOT overwritten; instead, +// cfg is added to the existing certificate's list of configs if not +// already in the list. Then all the names on cert are used to add +// entries to cfg.certificates (the config's name lookup map). +// Then the certificate is stored/updated in the cache. It returns +// a copy of the certificate that ends up being stored in the cache. +// +// It is VERY important, even for some test cases, that the Hash field +// of the cert be set properly. +// +// This function is safe for concurrent use. +func (cfg *Config) cacheCertificate(cert Certificate) Certificate { + cfg.certCache.mu.Lock() + defer cfg.certCache.mu.Unlock() + + // if this certificate already exists in the cache, + // use it instead of overwriting it -- very important! + if existingCert, ok := cfg.certCache.cache[cert.Hash]; ok { + cert = existingCert + } + + // attach this config to the certificate so we know which + // configs are referencing/using the certificate, but don't + // duplicate entries + var found bool + for _, c := range cert.configs { + if c == cfg { + found = true + break + } + } + if !found { + cert.configs = append(cert.configs, cfg) + } + + // key the certificate by all its names for this config only, + // this is how we find the certificate during handshakes + // (yes, if certs overlap in the names they serve, one will + // overwrite another here, but that's just how it goes) + for _, name := range cert.Names { + cfg.certificates[name] = cert.Hash + } + + // store the certificate + cfg.certCache.cache[cert.Hash] = cert + + return cert +} + +// HostQualifies returns true if the hostname alone +// appears eligible for automagic TLS. For example: +// localhost, empty hostname, and IP addresses are +// not eligible because we cannot obtain certificates +// for those names. Wildcard names are allowed, as long +// as they conform to CABF requirements (only one wildcard +// label, and it must be the left-most label). +func HostQualifies(hostname string) bool { + return hostname != "localhost" && // localhost is ineligible + + // hostname must not be empty + strings.TrimSpace(hostname) != "" && + + // only one wildcard label allowed, and it must be left-most + (!strings.Contains(hostname, "*") || + (strings.Count(hostname, "*") == 1 && + strings.HasPrefix(hostname, "*."))) && + + // must not start or end with a dot + !strings.HasPrefix(hostname, ".") && + !strings.HasSuffix(hostname, ".") && + + // cannot be an IP address, see + // https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt + net.ParseIP(hostname) == nil +} diff --git a/certmagic.go b/certmagic.go new file mode 100644 index 0000000..b112209 --- /dev/null +++ b/certmagic.go @@ -0,0 +1,511 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "crypto/tls" + "errors" + "fmt" + "log" + "net" + "net/http" + "net/url" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/xenolf/lego/acme" +) + +// HTTPS serves mux on domain name using the HTTP and HTTPS +// ports, redirecting all HTTP requests to HTTPS. +// +// Calling this function signifies your acceptance to +// the CA's Subscriber Agreement and/or Terms of Service. +func HTTPS(domainNames []string, mux http.Handler) error { + if mux == nil { + mux = http.DefaultServeMux + } + + cfg, err := manageWithDefaultConfig(domainNames, false) + if err != nil { + return err + } + + httpWg.Add(1) + defer httpWg.Done() + + // if we haven't made listeners yet, do so now, + // and clean them up when all servers are done + lnMu.Lock() + if httpLn == nil && httpsLn == nil { + httpLn, err = net.Listen("tcp", fmt.Sprintf(":%d", HTTPPort)) + if err != nil { + lnMu.Unlock() + return err + } + + httpsLn, err = tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig()) + if err != nil { + httpLn.Close() + httpLn = nil + lnMu.Unlock() + return err + } + + go func() { + httpWg.Wait() + lnMu.Lock() + httpLn.Close() + httpsLn.Close() + lnMu.Unlock() + }() + } + hln, hsln := httpLn, httpsLn + lnMu.Unlock() + + httpHandler := cfg.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler)) + + log.Printf("%v Serving HTTP->HTTPS on %s and %s", + domainNames, hln.Addr(), hsln.Addr()) + + go http.Serve(hln, httpHandler) + return http.Serve(hsln, mux) +} + +func httpRedirectHandler(w http.ResponseWriter, r *http.Request) { + toURL := "https://" + + // since we redirect to the standard HTTPS port, we + // do not need to include it in the redirect URL + requestHost, _, err := net.SplitHostPort(r.Host) + if err != nil { + requestHost = r.Host // host probably did not contain a port + } + + toURL += requestHost + toURL += r.URL.RequestURI() + + // get rid of this disgusting unencrypted HTTP connection 🤢 + w.Header().Set("Connection", "close") + + http.Redirect(w, r, toURL, http.StatusMovedPermanently) +} + +// TLS enables management of certificates for domainNames +// and returns a valid tls.Config. +// +// Because this is a convenience function that returns +// only a tls.Config, it does not assume HTTP is being +// served on the HTTP port, so the HTTP challenge is +// disabled (no HTTPChallengeHandler is necessary). +// +// Calling this function signifies your acceptance to +// the CA's Subscriber Agreement and/or Terms of Service. +func TLS(domainNames []string) (*tls.Config, error) { + cfg, err := manageWithDefaultConfig(domainNames, true) + return cfg.TLSConfig(), err +} + +// Listen manages certificates for domainName and returns a +// TLS listener. +// +// Because this convenience function returns only a TLS-enabled +// listener and does not presume HTTP is also being served, +// the HTTP challenge will be disabled. +// +// Calling this function signifies your acceptance to +// the CA's Subscriber Agreement and/or Terms of Service. +func Listen(domainNames []string) (net.Listener, error) { + cfg, err := manageWithDefaultConfig(domainNames, true) + if err != nil { + return nil, err + } + return tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig()) +} + +// Manage obtains certificates for domainNames and keeps them +// renewed using the returned Config. +// +// You will need to ensure that you use a TLS config that gets +// certificates from this Config and that the HTTP and TLS-ALPN +// challenges can be solved. The easiest way to do this is to +// use cfg.TLSConfig() as your TLS config and to wrap your +// HTTP handler with cfg.HTTPChallengeHandler(). If you don't +// have an HTTP server, you will need to disable the HTTP +// challenge. +// +// If you already have a TLS config you want to use, you can +// simply set its GetCertificate field to cfg.GetCertificate. +// +// Calling this function signifies your acceptance to +// the CA's Subscriber Agreement and/or Terms of Service. +func Manage(domainNames []string) (cfg *Config, err error) { + return manageWithDefaultConfig(domainNames, false) +} + +// manageWithDefaultConfig returns a TLS configuration that +// is fully managed for the given names, optionally +// with the HTTP challenge disabled. +func manageWithDefaultConfig(domainNames []string, disableHTTPChallenge bool) (*Config, error) { + cfg := NewDefault() + cfg.DisableHTTPChallenge = disableHTTPChallenge + return cfg, cfg.Manage(domainNames) +} + +// Locker facilitates synchronization of certificate tasks across +// machines and networks. +type Locker interface { + // TryLock will attempt to acquire the lock for key. If a + // lock could be obtained, nil values are returned as no + // waiting is required. If not (meaning another process is + // already working on key), a Waiter value will be returned, + // upon which you should Wait() until it is finished. + // + // The key should be a carefully-chosen value that uniquely + // and precisely identifies the operation being locked. For + // example, if it is for a certificate obtain or renew with + // the ACME protocol to the same CA endpoint (remembering + // that an obtain and renew are the same according to ACME, + // thus both obtain and renew should share a lock key), a + // good key would identify that operation by some name, + // concatenated with the domain name and the CA endpoint. + // + // TryLock never blocks; it always returns without waiting. + // + // To prevent deadlocks, all implementations (where this concern + // is relevant) should put a reasonable expiration on the lock in + // case Unlock is unable to be called due to some sort of storage + // system failure or crash. + TryLock(key string) (Waiter, error) + + // Unlock releases the lock for key. This method must ONLY be + // called after a successful call to TryLock where no Waiter was + // returned, and only after the operation requiring the lock is + // finished, even if it returned an error or timed out. Unlock + // should also clean up any unused resources allocated during + // TryLock. + Unlock(key string) error +} + +// Waiter is a type that can block until a lock is released. +type Waiter interface { + Wait() +} + +// OnDemandConfig contains some state relevant for providing +// on-demand TLS. +type OnDemandConfig struct { + // If set, this function will be the absolute + // authority on whether the hostname (according + // to SNI) is allowed to try to get a cert. + DecisionFunc func(name string) error + + // If no DecisionFunc is set, this whitelist + // is the absolute authority as to whether + // a certificate should be allowed to be tried. + // Names are compared against SNI value. + HostWhitelist []string + + // If no DecisionFunc or HostWhitelist are set, + // then an HTTP request will be made to AskURL + // to determine if a certificate should be + // obtained. If the request fails or the response + // is anything other than 2xx status code, the + // issuance will be denied. + AskURL *url.URL + + // If no DecisionFunc, HostWhitelist, or AskURL + // are set, then only this many certificates may + // be obtained on-demand; this field is required + // if all others are empty, otherwise, all cert + // issuances will fail. + MaxObtain int32 + + // The number of certificates that have been issued on-demand + // by this config. It is only safe to modify this count atomically. + // If it reaches MaxObtain, on-demand issuances must fail. + obtainedCount int32 +} + +// Allowed returns whether the issuance for name is allowed according to o. +func (o *OnDemandConfig) Allowed(name string) error { + // The decision function has absolute authority, if set + if o.DecisionFunc != nil { + return o.DecisionFunc(name) + } + + // Otherwise, the host whitelist has decision authority + if len(o.HostWhitelist) > 0 { + return o.checkWhitelistForObtainingNewCerts(name) + } + + // Otherwise, a URL is checked for permission to issue this cert + if o.AskURL != nil { + return o.checkURLForObtainingNewCerts(name) + } + + // Otherwise use the limit defined by the "max_certs" setting + return o.checkLimitsForObtainingNewCerts(name) +} + +func (o *OnDemandConfig) whitelistContains(name string) bool { + for _, n := range o.HostWhitelist { + if strings.ToLower(n) == strings.ToLower(name) { + return true + } + } + return false +} + +func (o *OnDemandConfig) checkWhitelistForObtainingNewCerts(name string) error { + if !o.whitelistContains(name) { + return fmt.Errorf("%s: name is not whitelisted", name) + } + return nil +} + +func (o *OnDemandConfig) checkURLForObtainingNewCerts(name string) error { + client := http.Client{ + Timeout: 10 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return errors.New("following http redirects is not allowed") + }, + } + + // Copy the URL from the config in order to modify it for this request + askURL := new(url.URL) + *askURL = *o.AskURL + + query := askURL.Query() + query.Set("domain", name) + askURL.RawQuery = query.Encode() + + resp, err := client.Get(askURL.String()) + if err != nil { + return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", o.AskURL, name, err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("certificate for hostname '%s' not allowed, non-2xx status code %d returned from %v", name, resp.StatusCode, o.AskURL) + } + + return nil +} + +// checkLimitsForObtainingNewCerts checks to see if name can be issued right +// now according the maximum count defined in the configuration. If a non-nil +// error is returned, do not issue a new certificate for name. +func (o *OnDemandConfig) checkLimitsForObtainingNewCerts(name string) error { + if o.MaxObtain == 0 { + return fmt.Errorf("%s: no certificates allowed to be issued on-demand", name) + } + + // User can set hard limit for number of certs for the process to issue + if o.MaxObtain > 0 && + atomic.LoadInt32(&o.obtainedCount) >= o.MaxObtain { + return fmt.Errorf("%s: maximum certificates issued (%d)", name, o.MaxObtain) + } + + // Make sure name hasn't failed a challenge recently + failedIssuanceMu.RLock() + when, ok := failedIssuance[name] + failedIssuanceMu.RUnlock() + if ok { + return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String()) + } + + // Make sure, if we've issued a few certificates already, that we haven't + // issued any recently + lastIssueTimeMu.Lock() + since := time.Since(lastIssueTime) + lastIssueTimeMu.Unlock() + if atomic.LoadInt32(&o.obtainedCount) >= 10 && since < 10*time.Minute { + return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since) + } + + // Good to go 👍 + return nil +} + +// failedIssuance is a set of names that we recently failed to get a +// certificate for from the ACME CA. They are removed after some time. +// When a name is in this map, do not issue a certificate for it on-demand. +var failedIssuance = make(map[string]time.Time) +var failedIssuanceMu sync.RWMutex + +// lastIssueTime records when we last obtained a certificate successfully. +// If this value is recent, do not make any on-demand certificate requests. +var lastIssueTime time.Time +var lastIssueTimeMu sync.Mutex + +// isLoopback returns true if the hostname of addr looks +// explicitly like a common local hostname. addr must only +// be a host or a host:port combination. +func isLoopback(addr string) bool { + host, _, err := net.SplitHostPort(strings.ToLower(addr)) + if err != nil { + host = addr // happens if the addr is only a hostname + } + return host == "localhost" || + strings.Trim(host, "[]") == "::1" || + strings.HasPrefix(host, "127.") +} + +// isInternal returns true if the IP of addr +// belongs to a private network IP range. addr +// must only be an IP or an IP:port combination. +// Loopback addresses are considered false. +func isInternal(addr string) bool { + privateNetworks := []string{ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "fc00::/7", + } + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr // happens if the addr is just a hostname, missing port + // if we encounter an error, the brackets need to be stripped + // because SplitHostPort didn't do it for us + host = strings.Trim(host, "[]") + } + ip := net.ParseIP(host) + if ip == nil { + return false + } + for _, privateNetwork := range privateNetworks { + _, ipnet, _ := net.ParseCIDR(privateNetwork) + if ipnet.Contains(ip) { + return true + } + } + return false +} + +// Package defaults +var ( + // The endpoint of the directory for the ACME + // CA we are to use + CA = LetsEncryptStagingCA // TODO: make production + + // The email address to use when creating or + // selecting an existing ACME server account + Email string + + // The synchronization implementation - all + // instances of certmagic in a cluster must + // use the same value here, otherwise some + // cert operations will not be properly + // coordinated + Sync Locker + + // Set to true if agreed to the CA's + // subscriber agreement + Agreed bool + + // Disable all HTTP challenges + DisableHTTPChallenge bool + + // Disable all TLS-ALPN challenges + DisableTLSALPNChallenge bool + + // How long before expiration to renew certificates + RenewDurationBefore = DefaultRenewDurationBefore + + // How long before expiration to require a renewed + // certificate when in interactive mode, like when + // the program is first starting up (see + // mholt/caddy#1680). A wider window between + // RenewDurationBefore and this value will suppress + // errors under duress (bad) but hopefully this duration + // will give it enough time for the blockage to be + // relieved. + RenewDurationBeforeAtStartup = DefaultRenewDurationBeforeAtStartup + + // An optional event callback clients can set + // to subscribe to certain things happening + // internally by this config; invocations are + // synchronous, so make them return quickly! + OnEvent func(event string, data interface{}) + + // The host (ONLY the host, not port) to listen + // on if necessary to start a listener to solve + // an ACME challenge + ListenHost string + + // The alternate port to use for the ACME HTTP + // challenge; if non-empty, this port will be + // used instead of HTTPChallengePort to spin up + // a listener for the HTTP challenge + AltHTTPPort int + + // The alternate port to use for the ACME + // TLS-ALPN challenge; the system must forward + // TLSALPNChallengePort to this port for + // challenge to succeed + AltTLSALPNPort int + + // The DNS provider to use when solving the + // ACME DNS challenge + DNSProvider acme.ChallengeProvider + + // The type of key to use when generating + // certificates + KeyType = acme.RSA2048 + + // The state needed to operate on-demand TLS + OnDemand *OnDemandConfig + + // Add the must staple TLS extension to the + // CSR generated by lego/acme + MustStaple bool +) + +const ( + // HTTPChallengePort is the officially-designated port for + // the HTTP challenge according to the ACME spec. + HTTPChallengePort = 80 + + // TLSALPNChallengePort is the officially-designated port for + // the TLS-ALPN challenge according to the ACME spec. + TLSALPNChallengePort = 443 +) + +// Some well-known CA endpoints available to use. +const ( + LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory" + LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory" +) + +// Port variables must remain their defaults unless you +// forward packets from the defaults to whatever these +// are set to; otherwise ACME challenges will fail. +var ( + // HTTPPort is the port on which to serve HTTP. + HTTPPort = 80 + + // HTTPSPort is the port on which to serve HTTPS. + HTTPSPort = 443 +) + +// Variables for conveniently serving HTTPS +var ( + httpLn, httpsLn net.Listener + lnMu sync.Mutex + httpWg sync.WaitGroup +) diff --git a/client.go b/client.go new file mode 100644 index 0000000..1c62c9f --- /dev/null +++ b/client.go @@ -0,0 +1,425 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "errors" + "fmt" + "log" + "net" + "net/url" + "strings" + "sync" + "time" + + "github.com/xenolf/lego/acme" +) + +// acmeMu ensures that only one ACME challenge occurs at a time. +var acmeMu sync.Mutex + +// acmeClient is a wrapper over acme.Client with +// some custom state attached. It is used to obtain, +// renew, and revoke certificates with ACME. +type acmeClient struct { + config *Config + acmeClient *acme.Client +} + +// listenerAddressInUse returns true if a TCP connection +// can be made to addr within a short time interval. +func listenerAddressInUse(addr string) bool { + conn, err := net.DialTimeout("tcp", addr, 250*time.Millisecond) + if err == nil { + conn.Close() + } + return err == nil +} + +func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) { + // look up or create the user account + leUser, err := cfg.getUser(cfg.Email) + if err != nil { + return nil, err + } + + // ensure key type is set + keyType := KeyType + if cfg.KeyType != "" { + keyType = cfg.KeyType + } + + // ensure CA URL (directory endpoint) is set + caURL := CA + if cfg.CA != "" { + caURL = cfg.CA + } + + // ensure endpoint is secure (assume HTTPS if scheme is missing) + if !strings.Contains(caURL, "://") { + caURL = "https://" + caURL + } + u, err := url.Parse(caURL) + if err != nil { + return nil, err + } + + if u.Scheme != "https" && !isLoopback(u.Host) && !isInternal(u.Host) { + return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL) + } + + clientKey := caURL + leUser.Email + string(keyType) + + // if an underlying client with this configuration already exists, reuse it + cfg.acmeClientsMu.Lock() + client, ok := cfg.acmeClients[clientKey] + if !ok { + // the client facilitates our communication with the CA server + client, err = acme.NewClient(caURL, &leUser, keyType) + if err != nil { + cfg.acmeClientsMu.Unlock() + return nil, err + } + cfg.acmeClients[clientKey] = client + } + cfg.acmeClientsMu.Unlock() + + // if not registered, the user must register an account + // with the CA and agree to terms + if leUser.Registration == nil { + if interactive { // can't prompt a user who isn't there + termsURL := client.GetToSURL() + if !cfg.Agreed && termsURL != "" { + cfg.Agreed = cfg.askUserAgreement(client.GetToSURL()) + } + if !cfg.Agreed && termsURL != "" { + return nil, errors.New("user must agree to CA terms (use -agree flag)") + } + } + + reg, err := client.Register(cfg.Agreed) + if err != nil { + return nil, errors.New("registration error: " + err.Error()) + } + leUser.Registration = reg + + // persist the user to storage + err = cfg.saveUser(leUser) + if err != nil { + return nil, errors.New("could not save user: " + err.Error()) + } + } + + c := &acmeClient{ + config: cfg, + acmeClient: client, + } + + if cfg.DNSProvider == nil { + // Use HTTP and TLS-ALPN challenges by default + + // figure out which ports we'll be serving the challenges on + useHTTPPort := HTTPChallengePort + useTLSALPNPort := TLSALPNChallengePort + if cfg.AltHTTPPort > 0 { + useHTTPPort = cfg.AltHTTPPort + } + if cfg.AltTLSALPNPort > 0 { + useTLSALPNPort = cfg.AltTLSALPNPort + } + + // If this machine is already listening on the HTTP or TLS-ALPN port + // designated for the challenges, then we need to handle the challenges + // a little differently: for HTTP, we will answer the challenge request + // using our own HTTP handler (the HandleHTTPChallenge function - this + // works only because challenge info is written to storage associated + // with cfg when the challenge is initiated); for TLS-ALPN, we will add + // the challenge cert to our cert cache and serve it up during the + // handshake. As for the default solvers... we are careful to honor the + // listener bind preferences by using cfg.ListenHost. + var httpSolver, alpnSolver acme.ChallengeProvider + httpSolver = acme.NewHTTPProviderServer(cfg.ListenHost, fmt.Sprintf("%d", useHTTPPort)) + alpnSolver = acme.NewTLSALPNProviderServer(cfg.ListenHost, fmt.Sprintf("%d", useTLSALPNPort)) + if listenerAddressInUse(net.JoinHostPort(cfg.ListenHost, fmt.Sprintf("%d", useHTTPPort))) { + httpSolver = nil + } + if listenerAddressInUse(net.JoinHostPort(cfg.ListenHost, fmt.Sprintf("%d", useTLSALPNPort))) { + alpnSolver = tlsALPNSolver{certCache: cfg.certCache} + } + + // because of our nifty Storage interface, we can distribute the HTTP and + // TLS-ALPN challenges across all instances that share the same storage - + // in fact, this is required now for successful solving of the HTTP challenge + // if the port is already in use, since we must write the challenge info + // to storage for the HTTPChallengeHandler to solve it successfully + c.acmeClient.SetChallengeProvider(acme.HTTP01, distributedSolver{ + config: cfg, + providerServer: httpSolver, + }) + c.acmeClient.SetChallengeProvider(acme.TLSALPN01, distributedSolver{ + config: cfg, + providerServer: alpnSolver, + }) + + // disable any challenges that should not be used + var disabledChallenges []acme.Challenge + if cfg.DisableHTTPChallenge { + disabledChallenges = append(disabledChallenges, acme.HTTP01) + } + if cfg.DisableTLSALPNChallenge { + disabledChallenges = append(disabledChallenges, acme.TLSALPN01) + } + if len(disabledChallenges) > 0 { + c.acmeClient.ExcludeChallenges(disabledChallenges) + } + } else { + // Otherwise, use DNS challenge exclusively + c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSALPN01}) + c.acmeClient.SetChallengeProvider(acme.DNS01, cfg.DNSProvider) + } + + return c, nil +} + +func (cfg *Config) lockKey(op, domainName string) string { + return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA) +} + +// Obtain obtains a single certificate for name. It stores the certificate +// on the disk if successful. This function is safe for concurrent use. +// +// Right now our storage mechanism only supports one name per certificate, +// so this function (along with Renew and Revoke) only accepts one domain +// as input. It can be easily modified to support SAN certificates if our +// storage mechanism is upgraded later. +// +// Callers who have access to a Config value should use the ObtainCert +// method on that instead of this lower-level method. +func (c *acmeClient) Obtain(name string) error { + if c.config.Sync != nil { + lockKey := c.config.lockKey("cert_acme", name) + waiter, err := c.config.Sync.TryLock(lockKey) + if err != nil { + return err + } + if waiter != nil { + log.Printf("[INFO] Certificate for %s is already being obtained elsewhere and stored; waiting", name) + waiter.Wait() + return nil // we assume the process with the lock succeeded, rather than hammering this execution path again + } + defer func() { + if err := c.config.Sync.Unlock(lockKey); err != nil { + log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err) + } + }() + } + + for attempts := 0; attempts < 2; attempts++ { + namesObtaining.Add([]string{name}) + acmeMu.Lock() + certificate, err := c.acmeClient.ObtainCertificate([]string{name}, true, nil, c.config.MustStaple) + acmeMu.Unlock() + namesObtaining.Remove([]string{name}) + if err != nil { + // for a certain kind of error, we can enumerate the error per-domain + if failures, ok := err.(acme.ObtainError); ok && len(failures) > 0 { + var errMsg string // combine all the failures into a single error message + for errDomain, obtainErr := range failures { + if obtainErr == nil { + continue + } + errMsg += fmt.Sprintf("[%s] failed to get certificate: %v\n", errDomain, obtainErr) + } + return errors.New(errMsg) + } + + return fmt.Errorf("[%s] failed to obtain certificate: %v", name, err) + } + + // double-check that we actually got a certificate, in case there's a bug upstream (see issue mholt/caddy#2121) + if certificate.Domain == "" || certificate.Certificate == nil { + return errors.New("returned certificate was empty; probably an unchecked error obtaining it") + } + + // Success - immediately save the certificate resource + err = c.config.saveCertResource(certificate) + if err != nil { + return fmt.Errorf("error saving assets for %v: %v", name, err) + } + + break + } + + if c.config.OnEvent != nil { + c.config.OnEvent("acme_cert_obtained", name) + } + + return nil +} + +// Renew renews the managed certificate for name. It puts the renewed +// certificate into storage (not the cache). This function is safe for +// concurrent use. +// +// Callers who have access to a Config value should use the RenewCert +// method on that instead of this lower-level method. +func (c *acmeClient) Renew(name string) error { + if c.config.Sync != nil { + lockKey := c.config.lockKey("cert_acme", name) + waiter, err := c.config.Sync.TryLock(lockKey) + if err != nil { + return err + } + if waiter != nil { + log.Printf("[INFO] Certificate for %s is already being renewed elsewhere and stored; waiting", name) + waiter.Wait() + return nil // assume that the worker that renewed the cert succeeded; avoid hammering this path over and over + } + defer func() { + if err := c.config.Sync.Unlock(lockKey); err != nil { + log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err) + } + }() + } + + // Prepare for renewal (load PEM cert, key, and meta) + certRes, err := c.config.loadCertResource(name) + if err != nil { + return err + } + + // Perform renewal and retry if necessary, but not too many times. + var newCertMeta *acme.CertificateResource + var success bool + for attempts := 0; attempts < 2; attempts++ { + namesObtaining.Add([]string{name}) + acmeMu.Lock() + newCertMeta, err = c.acmeClient.RenewCertificate(certRes, true, c.config.MustStaple) + acmeMu.Unlock() + namesObtaining.Remove([]string{name}) + if err == nil { + // double-check that we actually got a certificate; check a couple fields, just in case + if newCertMeta == nil || newCertMeta.Domain == "" || newCertMeta.Certificate == nil { + err = errors.New("returned certificate was empty; probably an unchecked error renewing it") + } else { + success = true + break + } + } + + // wait a little bit and try again + wait := 10 * time.Second + log.Printf("[ERROR] Renewing [%v]: %v; trying again in %s", name, err, wait) + time.Sleep(wait) + } + + if !success { + return errors.New("too many renewal attempts; last error: " + err.Error()) + } + + if c.config.OnEvent != nil { + c.config.OnEvent("acme_cert_renewed", name) + } + + return c.config.saveCertResource(newCertMeta) +} + +// Revoke revokes the certificate for name and deletes +// it from storage. +func (c *acmeClient) Revoke(name string) error { + if !c.config.certCache.storage.Exists(prefixSiteKey(c.config.CA, name)) { + return fmt.Errorf("private key not found for %s", name) + } + + certRes, err := c.config.loadCertResource(name) + if err != nil { + return err + } + + err = c.acmeClient.RevokeCertificate(certRes.Certificate) + if err != nil { + return err + } + + if c.config.OnEvent != nil { + c.config.OnEvent("acme_cert_revoked", name) + } + + err = c.config.certCache.storage.Delete(prefixSiteCert(c.config.CA, name)) + if err != nil { + return fmt.Errorf("certificate revoked, but unable to delete certificate file: %v", err) + } + err = c.config.certCache.storage.Delete(prefixSiteKey(c.config.CA, name)) + if err != nil { + return fmt.Errorf("certificate revoked, but unable to delete private key: %v", err) + } + err = c.config.certCache.storage.Delete(prefixSiteMeta(c.config.CA, name)) + if err != nil { + return fmt.Errorf("certificate revoked, but unable to delete certificate metadata: %v", err) + } + + return nil +} + +// namesObtaining is a set of hostnames with thread-safe +// methods. A name should be in this set only while this +// package is in the process of obtaining a certificate +// for the name. ACME challenges that are received for +// names which are not in this set were not initiated by +// this package and probably should not be handled by +// this package. +var namesObtaining = nameCoordinator{names: make(map[string]struct{})} + +type nameCoordinator struct { + names map[string]struct{} + mu sync.RWMutex +} + +// Add adds names to c. It is safe for concurrent use. +func (c *nameCoordinator) Add(names []string) { + c.mu.Lock() + for _, name := range names { + c.names[strings.ToLower(name)] = struct{}{} + } + c.mu.Unlock() +} + +// Remove removes names from c. It is safe for concurrent use. +func (c *nameCoordinator) Remove(names []string) { + c.mu.Lock() + for _, name := range names { + delete(c.names, strings.ToLower(name)) + } + c.mu.Unlock() +} + +// Has returns true if c has name. It is safe for concurrent use. +func (c *nameCoordinator) Has(name string) bool { + hostname, _, err := net.SplitHostPort(name) + if err != nil { + hostname = name + } + c.mu.RLock() + _, ok := c.names[strings.ToLower(hostname)] + c.mu.RUnlock() + return ok +} + +// KnownACMECAs is a list of ACME directory endpoints of +// known, public, and trusted ACME-compatible certificate +// authorities. +var KnownACMECAs = []string{ + "https://acme-v02.api.letsencrypt.org/directory", +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..5855b09 --- /dev/null +++ b/config.go @@ -0,0 +1,364 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "crypto/tls" + "fmt" + "strings" + "sync" + "time" + + "github.com/xenolf/lego/acme" +) + +// Config configures a certificate manager instance. +// An empty Config is not valid: use New() to obtain +// a valid Config. +type Config struct { + // The endpoint of the directory for the ACME + // CA we are to use + CA string + + // The email address to use when creating or + // selecting an existing ACME server account + Email string + + // The synchronization implementation - although + // it is not strictly required to have a Sync + // value in general, all instances running in + // in a cluster for the same domain names must + // specify a Sync and use the same one, otherwise + // some cert operations will not be properly + // coordinated + Sync Locker + + // Set to true if agreed to the CA's + // subscriber agreement + Agreed bool + + // Disable all HTTP challenges + DisableHTTPChallenge bool + + // Disable all TLS-ALPN challenges + DisableTLSALPNChallenge bool + + // How long before expiration to renew certificates + RenewDurationBefore time.Duration + + // How long before expiration to require a renewed + // certificate when in interactive mode, like when + // the program is first starting up (see + // mholt/caddy#1680). A wider window between + // RenewDurationBefore and this value will suppress + // errors under duress (bad) but hopefully this duration + // will give it enough time for the blockage to be + // relieved. + RenewDurationBeforeAtStartup time.Duration + + // An optional event callback clients can set + // to subscribe to certain things happening + // internally by this config; invocations are + // synchronous, so make them return quickly! + OnEvent func(event string, data interface{}) + + // The host (ONLY the host, not port) to listen + // on if necessary to start a listener to solve + // an ACME challenge + ListenHost string + + // The alternate port to use for the ACME HTTP + // challenge; if non-empty, this port will be + // used instead of HTTPChallengePort to spin up + // a listener for the HTTP challenge + AltHTTPPort int + + // The alternate port to use for the ACME + // TLS-ALPN challenge; the system must forward + // TLSALPNChallengePort to this port for + // challenge to succeed + AltTLSALPNPort int + + // The DNS provider to use when solving the + // ACME DNS challenge + DNSProvider acme.ChallengeProvider + + // The type of key to use when generating + // certificates + KeyType acme.KeyType + + // The state needed to operate on-demand TLS + OnDemand *OnDemandConfig + + // Add the must staple TLS extension to the + // CSR generated by lego/acme + MustStaple bool + + // Map of hostname to certificate hash; used + // to complete handshakes and serve the right + // certificate given SNI + certificates map[string]string + + // Pointer to the certificate store to use + certCache *Cache + + // Map of client config key to ACME clients + // so they can be reused + acmeClients map[string]*acme.Client + acmeClientsMu *sync.Mutex +} + +// NewDefault returns a new, valid, default config. +// +// Calling this function signifies your acceptance to +// the CA's Subscriber Agreement and/or Terms of Service. +func NewDefault() *Config { + return New(Config{Agreed: true}) +} + +// New makes a valid config based on cfg and uses +// a default certificate cache. All calls to +// New() will use the same certificate cache. +func New(cfg Config) *Config { + return NewWithCache(defaultCache, cfg) +} + +// NewWithCache makes a valid new config based on cfg +// and uses the provided certificate cache. +func NewWithCache(certCache *Cache, cfg Config) *Config { + // avoid nil pointers with sensible defaults + if certCache == nil { + certCache = defaultCache + } + if certCache.storage == nil { + certCache.storage = DefaultStorage + } + + // fill in default values + if cfg.CA == "" { + cfg.CA = CA + } + if cfg.Email == "" { + cfg.Email = Email + } + if cfg.OnDemand == nil { + cfg.OnDemand = OnDemand + } + if !cfg.Agreed { + cfg.Agreed = Agreed + } + if !cfg.DisableHTTPChallenge { + cfg.DisableHTTPChallenge = DisableHTTPChallenge + } + if !cfg.DisableTLSALPNChallenge { + cfg.DisableTLSALPNChallenge = DisableTLSALPNChallenge + } + if cfg.RenewDurationBefore == 0 { + cfg.RenewDurationBefore = RenewDurationBefore + } + if cfg.RenewDurationBeforeAtStartup == 0 { + cfg.RenewDurationBeforeAtStartup = RenewDurationBeforeAtStartup + } + if cfg.OnEvent == nil { + cfg.OnEvent = OnEvent + } + if cfg.ListenHost == "" { + cfg.ListenHost = ListenHost + } + if cfg.AltHTTPPort == 0 { + cfg.AltHTTPPort = AltHTTPPort + } + if cfg.AltTLSALPNPort == 0 { + cfg.AltTLSALPNPort = AltTLSALPNPort + } + if cfg.DNSProvider == nil { + cfg.DNSProvider = DNSProvider + } + if cfg.KeyType == "" { + cfg.KeyType = KeyType + } + if cfg.OnDemand == nil { + cfg.OnDemand = OnDemand + } + if !cfg.MustStaple { + cfg.MustStaple = MustStaple + } + + // if no sync facility is provided, we'll default to + // a file system synchronizer backed by the storage + // given to certCache (if it is one), or just a simple + // in-memory sync facility otherwise (strictly speaking, + // a sync is not required; only if running multiple + // instances for the same domain names concurrently) + if cfg.Sync == nil { + if ccfs, ok := certCache.storage.(FileStorage); ok { + cfg.Sync = NewFileStorageLocker(ccfs) + } else { + cfg.Sync = NewMemoryLocker() + } + } + + // ensure the unexported fields are valid + cfg.certificates = make(map[string]string) + cfg.certCache = certCache + cfg.acmeClients = make(map[string]*acme.Client) + cfg.acmeClientsMu = new(sync.Mutex) + + return &cfg +} + +// Manage causes the certificates for domainNames to be managed +// according to cfg. +func (cfg *Config) Manage(domainNames []string) error { + for _, domainName := range domainNames { + // if on-demand is configured, simply whitelist this name + if cfg.OnDemand != nil { + if !cfg.OnDemand.whitelistContains(domainName) { + cfg.OnDemand.HostWhitelist = append(cfg.OnDemand.HostWhitelist, domainName) + } + continue + } + + // try loading an existing certificate; if it doesn't + // exist yet, obtain one and try loading it again + cert, err := cfg.CacheManagedCertificate(domainName) + if err != nil { + if _, ok := err.(ErrNotExist); ok { + // if it doesn't exist, get it, then try loading it again + err := cfg.ObtainCert(domainName, false) + if err != nil { + return fmt.Errorf("%s: obtaining certificate: %v", domainName, err) + } + cert, err = cfg.CacheManagedCertificate(domainName) + if err != nil { + return fmt.Errorf("%s: caching certificate after obtaining it: %v", domainName, err) + } + continue + } + return fmt.Errorf("%s: caching certificate: %v", domainName, err) + } + + // for existing certificates, make sure it is renewed + if cert.NeedsRenewal() { + err := cfg.RenewCert(domainName, false) + if err != nil { + return fmt.Errorf("%s: renewing certificate: %v", domainName, err) + } + } + } + + return nil +} + +// ObtainCert obtains a certificate for name using c, as long +// as a certificate does not already exist in storage for that +// name. The name must qualify and c must be flagged as Managed. +// This function is a no-op if storage already has a certificate +// for name. +// +// It only obtains and stores certificates (and their keys), +// it does not load them into memory. If interactive is true, +// the user may be shown a prompt. +func (cfg *Config) ObtainCert(name string, interactive bool) error { + skip, err := cfg.preObtainOrRenewChecks(name, interactive) + if err != nil { + return err + } + if skip { + return nil + } + + // we expect this to be a new site + if cfg.certCache.storage.Exists(prefixSiteCert(cfg.CA, name)) { + return nil + } + + client, err := cfg.newACMEClient(interactive) + if err != nil { + return err + } + return client.Obtain(name) +} + +// RenewCert renews the certificate for name using cfg. It stows the +// renewed certificate and its assets in storage if successful. +func (cfg *Config) RenewCert(name string, interactive bool) error { + skip, err := cfg.preObtainOrRenewChecks(name, interactive) + if err != nil { + return err + } + if skip { + return nil + } + client, err := cfg.newACMEClient(interactive) + if err != nil { + return err + } + return client.Renew(name) +} + +// RevokeCert revokes the certificate for domain via ACME protocol. +func (cfg *Config) RevokeCert(domain string, interactive bool) error { + client, err := cfg.newACMEClient(interactive) + if err != nil { + return err + } + return client.Revoke(domain) +} + +// TLSConfig returns a TLS configuration that +// can be used to configure TLS listeners. It +// supports the TLS-ALPN challenge and serves +// up certificates managed by cfg. +func (cfg *Config) TLSConfig() *tls.Config { + return &tls.Config{ + GetCertificate: cfg.GetCertificate, + NextProtos: []string{"h2", "http/1.1", acme.ACMETLS1Protocol}, + } +} + +// RenewAllCerts triggers a renewal check of all +// certificates in the cache. It only renews +// certificates if they need to be renewed. +// func (cfg *Config) RenewAllCerts(interactive bool) error { +// return cfg.certCache.RenewManagedCertificates(interactive) +// } + +// preObtainOrRenewChecks perform a few simple checks before +// obtaining or renewing a certificate with ACME, and returns +// whether this name should be skipped (like if it's not +// managed TLS) as well as any error. It ensures that the +// config is Managed, that the name qualifies for a certificate, +// and that an email address is available. +func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, error) { + if !HostQualifies(name) { + return true, nil + } + + // wildcard certificates require DNS challenge (as of March 2018) + if strings.Contains(name, "*") && cfg.DNSProvider == nil { + return false, fmt.Errorf("wildcard domain name (%s) requires DNS challenge; use dns subdirective to configure it", name) + } + + if cfg.Email == "" { + var err error + cfg.Email, err = cfg.getEmail(allowPrompts) + if err != nil { + return false, err + } + } + + return false, nil +} diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..c402fa9 --- /dev/null +++ b/crypto.go @@ -0,0 +1,155 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "hash/fnv" + + "github.com/xenolf/lego/acme" +) + +// encodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes. +func encodePrivateKey(key crypto.PrivateKey) ([]byte, error) { + var pemType string + var keyBytes []byte + switch key := key.(type) { + case *ecdsa.PrivateKey: + var err error + pemType = "EC" + keyBytes, err = x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + case *rsa.PrivateKey: + pemType = "RSA" + keyBytes = x509.MarshalPKCS1PrivateKey(key) + } + pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} + return pem.EncodeToMemory(&pemKey), nil +} + +// decodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. +func decodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) { + keyBlock, _ := pem.Decode(keyPEMBytes) + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + } + return nil, fmt.Errorf("unknown private key type") +} + +// parseCertsFromPEMBundle parses a certificate bundle from top to bottom and returns +// a slice of x509 certificates. This function will error if no certificates are found. +func parseCertsFromPEMBundle(bundle []byte) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate + var certDERBlock *pem.Block + for { + certDERBlock, bundle = pem.Decode(bundle) + if certDERBlock == nil { + break + } + if certDERBlock.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return nil, err + } + certificates = append(certificates, cert) + } + } + if len(certificates) == 0 { + return nil, fmt.Errorf("no certificates found in bundle") + } + return certificates, nil +} + +// fastHash hashes input using a hashing algorithm that +// is fast, and returns the hash as a hex-encoded string. +// Do not use this for cryptographic purposes. +func fastHash(input []byte) string { + h := fnv.New32a() + h.Write(input) + return fmt.Sprintf("%x", h.Sum32()) +} + +// saveCertResource saves the certificate resource to disk. This +// includes the certificate file itself, the private key, and the +// metadata file. +func (cfg *Config) saveCertResource(cert *acme.CertificateResource) error { + metaBytes, err := json.MarshalIndent(&cert, "", "\t") + if err != nil { + return fmt.Errorf("encoding certificate metadata: %v", err) + } + + all := []keyValue{ + { + key: prefixSiteCert(cfg.CA, cert.Domain), + value: cert.Certificate, + }, + { + key: prefixSiteKey(cfg.CA, cert.Domain), + value: cert.PrivateKey, + }, + { + key: prefixSiteMeta(cfg.CA, cert.Domain), + value: metaBytes, + }, + } + + return storeTx(cfg.certCache.storage, all) +} + +func (cfg *Config) loadCertResource(domain string) (acme.CertificateResource, error) { + var certRes acme.CertificateResource + certBytes, err := cfg.certCache.storage.Load(prefixSiteCert(cfg.CA, domain)) + if err != nil { + return certRes, err + } + keyBytes, err := cfg.certCache.storage.Load(prefixSiteKey(cfg.CA, domain)) + if err != nil { + return certRes, err + } + metaBytes, err := cfg.certCache.storage.Load(prefixSiteMeta(cfg.CA, domain)) + if err != nil { + return certRes, err + } + err = json.Unmarshal(metaBytes, &certRes) + if err != nil { + return certRes, fmt.Errorf("decoding certificate metadata: %v", err) + } + certRes.Certificate = certBytes + certRes.PrivateKey = keyBytes + return certRes, nil +} + +// hashCertificateChain computes the unique hash of certChain, +// which is the chain of DER-encoded bytes. It returns the +// hex encoding of the hash. +func hashCertificateChain(certChain [][]byte) string { + h := sha256.New() + for _, certInChain := range certChain { + h.Write(certInChain) + } + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/filestorage.go b/filestorage.go new file mode 100644 index 0000000..4370494 --- /dev/null +++ b/filestorage.go @@ -0,0 +1,126 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" +) + +// FileStorage facilitates forming file paths derived from a root +// directory. It is used to get file paths in a consistent, +// cross-platform way or persisting ACME assets on the file system. +type FileStorage struct { + Path string +} + +// Exists returns true if key exists in fs. +func (fs FileStorage) Exists(key string) bool { + _, err := os.Stat(key) + return !os.IsNotExist(err) +} + +// Store saves value at key. +func (fs FileStorage) Store(key string, value []byte) error { + filename := fs.filename(key) + err := os.MkdirAll(filepath.Dir(filename), 0700) + if err != nil { + return err + } + return ioutil.WriteFile(filename, value, 0600) +} + +// Load retrieves the value at key. +func (fs FileStorage) Load(key string) ([]byte, error) { + contents, err := ioutil.ReadFile(fs.filename(key)) + if os.IsNotExist(err) { + return nil, ErrNotExist(err) + } + return contents, nil +} + +// Delete deletes the value at key. +// TODO: Delete any empty folders caused by this operation +func (fs FileStorage) Delete(key string) error { + err := os.Remove(fs.filename(key)) + if os.IsNotExist(err) { + return ErrNotExist(err) + } + return err +} + +// List returns all keys that match prefix. +func (fs FileStorage) List(prefix string) ([]string, error) { + d, err := os.Open(fs.filename(prefix)) + if os.IsNotExist(err) { + return nil, ErrNotExist(err) + } + if err != nil { + return nil, err + } + defer d.Close() + return d.Readdirnames(-1) +} + +// Stat returns information about key. +func (fs FileStorage) Stat(key string) (KeyInfo, error) { + fi, err := os.Stat(fs.filename(key)) + if os.IsNotExist(err) { + return KeyInfo{}, ErrNotExist(err) + } + if err != nil { + return KeyInfo{}, err + } + return KeyInfo{ + Key: key, + Modified: fi.ModTime(), + Size: fi.Size(), + }, nil +} + +func (fs FileStorage) filename(key string) string { + return filepath.Join(fs.Path, filepath.FromSlash(key)) +} + +// homeDir returns the best guess of the current user's home +// directory from environment variables. If unknown, "." (the +// current directory) is returned instead. +func homeDir() string { + home := os.Getenv("HOME") + if home == "" && runtime.GOOS == "windows" { + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home = drive + path + if drive == "" || path == "" { + home = os.Getenv("USERPROFILE") + } + } + if home == "" { + home = "." + } + return home +} + +func dataDir() string { + baseDir := filepath.Join(homeDir(), ".local", "share") + if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { + baseDir = xdgData + } + return filepath.Join(baseDir, "certmagic") +} + +var _ Storage = FileStorage{} diff --git a/filestoragesync.go b/filestoragesync.go new file mode 100644 index 0000000..e6c25de --- /dev/null +++ b/filestoragesync.go @@ -0,0 +1,146 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +// FileStorageLocker implements the Locker interface +// using the file system. An empty value is NOT VALID, +// so you must use NewFileStorageLocker() to get one. +type FileStorageLocker struct { + fs FileStorage +} + +// NewFileStorageLocker returns a valid Locker backed by fs. +func NewFileStorageLocker(fs FileStorage) *FileStorageLocker { + return &FileStorageLocker{fs: fs} +} + +// TryLock attempts to get a lock for name, otherwise it returns +// a Waiter value to wait until the other process is finished. +func (l *FileStorageLocker) TryLock(name string) (Waiter, error) { + fileStorageNameLocksMu.Lock() + defer fileStorageNameLocksMu.Unlock() + + // see if lock already exists within this process + fw, ok := fileStorageNameLocks[name] + if ok { + // lock already created within process, let caller wait on it + return fw, nil + } + + // attempt to persist lock to disk by creating lock file + + // parent dir must exist + lockDir := l.lockDir() + if err := os.MkdirAll(lockDir, 0700); err != nil { + return nil, err + } + + fw = &FileStorageWaiter{ + filename: filepath.Join(lockDir, safeKey(name)+".lock"), + wg: new(sync.WaitGroup), + } + + // create the file in a special mode such that an + // error is returned if it already exists + lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + if os.IsExist(err) { + // another process has the lock; use it to wait + return fw, nil + } + // otherwise, this was some unexpected error + return nil, err + } + lf.Close() + + // looks like we get the lock + fw.wg.Add(1) + fileStorageNameLocks[name] = fw + + return nil, nil +} + +// Unlock releases the lock for name. +func (l *FileStorageLocker) Unlock(name string) error { + fileStorageNameLocksMu.Lock() + defer fileStorageNameLocksMu.Unlock() + + fw, ok := fileStorageNameLocks[name] + if !ok { + return fmt.Errorf("FileStorageLocker: no lock to release for %s", name) + } + + // remove lock file + os.Remove(fw.filename) + + // if parent folder is now empty, remove it too to keep it tidy + dir, err := os.Open(l.lockDir()) // OK to ignore error here + if err == nil { + items, _ := dir.Readdirnames(3) // OK to ignore error here + if len(items) == 0 { + os.Remove(dir.Name()) + } + dir.Close() + } + + // clean up in memory + fw.wg.Done() + delete(fileStorageNameLocks, name) + + return nil +} + +func (l *FileStorageLocker) lockDir() string { + return filepath.Join(l.fs.Path, "locks") +} + +// FileStorageWaiter waits for a file to disappear; it +// polls the file system to check for the existence of +// a file. It also uses a WaitGroup to optimize the +// polling in the case when this process is the only +// one waiting. (Other processes that are waiting +// for the lock will still block, but must wait +// for the poll intervals to get their answer.) +type FileStorageWaiter struct { + filename string + wg *sync.WaitGroup +} + +// Wait waits until the lock is released. +func (fw *FileStorageWaiter) Wait() { + start := time.Now() + fw.wg.Wait() + for time.Since(start) < 1*time.Hour { + _, err := os.Stat(fw.filename) + if os.IsNotExist(err) { + return + } + time.Sleep(1 * time.Second) + } +} + +var fileStorageNameLocks = make(map[string]*FileStorageWaiter) +var fileStorageNameLocksMu sync.Mutex + +var _ Locker = &FileStorageLocker{} +var _ Waiter = &FileStorageWaiter{} diff --git a/handshake.go b/handshake.go new file mode 100644 index 0000000..bcd781a --- /dev/null +++ b/handshake.go @@ -0,0 +1,401 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/xenolf/lego/acme" +) + +// GetCertificate gets a certificate to satisfy clientHello. In getting +// the certificate, it abides the rules and settings defined in the +// Config that matches clientHello.ServerName. It first checks the in- +// memory cache, then, if the config enables "OnDemand", it accesses +// disk, then accesses the network if it must obtain a new certificate +// via ACME. +// +// This method is safe for use as a tls.Config.GetCertificate callback. +func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if cfg.OnEvent != nil { + cfg.OnEvent("tls_handshake_started", clientHello) + } + + // special case: serve up the certificate for a TLS-ALPN ACME challenge + // (https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05) + for _, proto := range clientHello.SupportedProtos { + if proto == acme.ACMETLS1Protocol { + cfg.certCache.mu.RLock() + challengeCert, ok := cfg.certCache.cache[tlsALPNCertKeyName(clientHello.ServerName)] + cfg.certCache.mu.RUnlock() + if !ok { + // see if this challenge was started in a cluster; try distributed challenge solver + // (note that the tls.Config's ALPN settings must include the ACME TLS-ALPN challenge + // protocol string, otherwise a valid certificate will not solve the challenge; we + // should already have taken care of that when we made the tls.Config) + challengeCert, ok, err := cfg.tryDistributedChallengeSolver(clientHello) + if err != nil { + log.Printf("[ERROR][%s] TLS-ALPN: %v", clientHello.ServerName, err) + } + if ok { + return &challengeCert.Certificate, nil + } + + return nil, fmt.Errorf("no certificate to complete TLS-ALPN challenge for SNI name: %s", clientHello.ServerName) + } + return &challengeCert.Certificate, nil + } + } + + // get the certificate and serve it up + cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) + if err == nil && cfg.OnEvent != nil { + cfg.OnEvent("tls_handshake_completed", clientHello) + } + return &cert.Certificate, err +} + +// getCertificate gets a certificate that matches name (a server name) +// from the in-memory cache, according to the lookup table associated with +// cfg. The lookup then points to a certificate in the Instance certificate +// cache. +// +// If there is no exact match for name, it will be checked against names of +// the form '*.example.com' (wildcard certificates) according to RFC 6125. +// If a match is found, matched will be true. If no matches are found, matched +// will be false and a "default" certificate will be returned with defaulted +// set to true. If defaulted is false, then no certificates were available. +// +// The logic in this function is adapted from the Go standard library, +// which is by the Go Authors. +// +// This function is safe for concurrent use. +func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defaulted bool) { + var certKey string + var ok bool + + // Not going to trim trailing dots here since RFC 3546 says, + // "The hostname is represented ... without a trailing dot." + // Just normalize to lowercase. + name = strings.ToLower(name) + + cfg.certCache.mu.RLock() + defer cfg.certCache.mu.RUnlock() + + // exact match? great, let's use it + if certKey, ok = cfg.certificates[name]; ok { + cert = cfg.certCache.cache[certKey] + matched = true + return + } + + // try replacing labels in the name with wildcards until we get a match + labels := strings.Split(name, ".") + for i := range labels { + labels[i] = "*" + candidate := strings.Join(labels, ".") + if certKey, ok = cfg.certificates[candidate]; ok { + cert = cfg.certCache.cache[certKey] + matched = true + return + } + } + + // check the certCache directly to see if the SNI name is + // already the key of the certificate it wants; this implies + // that the SNI can contain the hash of a specific cert + // (chain) it wants and we will still be able to serveit up + // (this behavior, by the way, could be controversial as to + // whether it complies with RFC 6066 about SNI, but I think + // it does, soooo...) + if directCert, ok := cfg.certCache.cache[name]; ok { + cert = directCert + matched = true + return + } + + // if nothing matches, use a "default" certificate (See issues + // mholt/caddy#2035 and mholt/caddy#1303; any change to this + // behavior must account for hosts defined like ":443" or + // "0.0.0.0:443" where the hostname is empty or a catch-all + // IP or something.) + if certKey, ok := cfg.certificates[""]; ok { + cert = cfg.certCache.cache[certKey] + defaulted = true + return + } + + return +} + +// getCertDuringHandshake will get a certificate for name. It first tries +// the in-memory cache. If no certificate for name is in the cache, the +// config most closely corresponding to name will be loaded. If that config +// allows it (OnDemand==true) and if loadIfNecessary == true, it goes to disk +// to load it into the cache and serve it. If it's not on disk and if +// obtainIfNecessary == true, the certificate will be obtained from the CA, +// cached, and served. If obtainIfNecessary is true, then loadIfNecessary +// must also be set to true. An error will be returned if and only if no +// certificate is available. +// +// This function is safe for concurrent use. +func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { + // First check our in-memory cache to see if we've already loaded it + cert, matched, defaulted := cfg.getCertificate(name) + if matched { + return cert, nil + } + + // If OnDemand is enabled, then we might be able to load or + // obtain a needed certificate + if cfg.OnDemand != nil && loadIfNecessary { + // Then check to see if we have one on disk + loadedCert, err := cfg.CacheManagedCertificate(name) + if err == nil { + loadedCert, err = cfg.handshakeMaintenance(name, loadedCert) + if err != nil { + log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err) + } + return loadedCert, nil + } + if obtainIfNecessary { + // By this point, we need to ask the CA for a certificate + + name = strings.ToLower(name) + + // Make sure the certificate should be obtained based on config + err := cfg.checkIfCertShouldBeObtained(name) + if err != nil { + return Certificate{}, err + } + + // Name has to qualify for a certificate + if !HostQualifies(name) { + return cert, errors.New("hostname '" + name + "' does not qualify for certificate") + } + + // Obtain certificate from the CA + return cfg.obtainOnDemandCertificate(name) + } + } + + // Fall back to the default certificate if there is one + if defaulted { + return cert, nil + } + + return Certificate{}, fmt.Errorf("no certificate available for %s", name) +} + +// checkIfCertShouldBeObtained checks to see if an on-demand tls certificate +// should be obtained for a given domain based upon the config settings. If +// a non-nil error is returned, do not issue a new certificate for name. +func (cfg *Config) checkIfCertShouldBeObtained(name string) error { + if cfg.OnDemand == nil { + return fmt.Errorf("not configured for on-demand certificate issuance") + } + return cfg.OnDemand.Allowed(name) +} + +// obtainOnDemandCertificate obtains a certificate for name for the given +// name. If another goroutine has already started obtaining a cert for +// name, it will wait and use what the other goroutine obtained. +// +// This function is safe for use by multiple concurrent goroutines. +func (cfg *Config) obtainOnDemandCertificate(name string) (Certificate, error) { + // We must protect this process from happening concurrently, so synchronize. + obtainCertWaitChansMu.Lock() + wait, ok := obtainCertWaitChans[name] + if ok { + // lucky us -- another goroutine is already obtaining the certificate. + // wait for it to finish obtaining the cert and then we'll use it. + obtainCertWaitChansMu.Unlock() + <-wait + return cfg.getCertDuringHandshake(name, true, false) + } + + // looks like it's up to us to do all the work and obtain the cert. + // make a chan others can wait on if needed + wait = make(chan struct{}) + obtainCertWaitChans[name] = wait + obtainCertWaitChansMu.Unlock() + + // obtain the certificate + log.Printf("[INFO] Obtaining new certificate for %s", name) + err := cfg.ObtainCert(name, false) + + // immediately unblock anyone waiting for it; doing this in + // a defer would risk deadlock because of the recursive call + // to getCertDuringHandshake below when we return! + obtainCertWaitChansMu.Lock() + close(wait) + delete(obtainCertWaitChans, name) + obtainCertWaitChansMu.Unlock() + + if err != nil { + // Failed to solve challenge, so don't allow another on-demand + // issue for this name to be attempted for a little while. + failedIssuanceMu.Lock() + failedIssuance[name] = time.Now() + go func(name string) { + time.Sleep(5 * time.Minute) + failedIssuanceMu.Lock() + delete(failedIssuance, name) + failedIssuanceMu.Unlock() + }(name) + failedIssuanceMu.Unlock() + return Certificate{}, err + } + + // Success - update counters and stuff + atomic.AddInt32(&cfg.OnDemand.obtainedCount, 1) + lastIssueTimeMu.Lock() + lastIssueTime = time.Now() + lastIssueTimeMu.Unlock() + + // certificate is already on disk; now just start over to load it and serve it + return cfg.getCertDuringHandshake(name, true, false) +} + +// handshakeMaintenance performs a check on cert for expiration and OCSP +// validity. +// +// This function is safe for use by multiple concurrent goroutines. +func (cfg *Config) handshakeMaintenance(name string, cert Certificate) (Certificate, error) { + // Check cert expiration + timeLeft := cert.NotAfter.Sub(time.Now().UTC()) + if timeLeft < cfg.RenewDurationBefore { + log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft) + return cfg.renewDynamicCertificate(name, cert) + } + + // Check OCSP staple validity + if cert.OCSP != nil { + refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2) + if time.Now().After(refreshTime) { + err := cfg.certCache.stapleOCSP(&cert, nil) + if err != nil { + // An error with OCSP stapling is not the end of the world, and in fact, is + // quite common considering not all certs have issuer URLs that support it. + log.Printf("[ERROR] Getting OCSP for %s: %v", name, err) + } + cfg.certCache.mu.Lock() + cfg.certCache.cache[cert.Hash] = cert + cfg.certCache.mu.Unlock() + } + } + + return cert, nil +} + +// renewDynamicCertificate renews the certificate for name using cfg. It returns the +// certificate to use and an error, if any. name should already be lower-cased before +// calling this function. name is the name obtained directly from the handshake's +// ClientHello. +// +// This function is safe for use by multiple concurrent goroutines. +func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate) (Certificate, error) { + obtainCertWaitChansMu.Lock() + wait, ok := obtainCertWaitChans[name] + if ok { + // lucky us -- another goroutine is already renewing the certificate. + // wait for it to finish, then we'll use the new one. + obtainCertWaitChansMu.Unlock() + <-wait + return cfg.getCertDuringHandshake(name, true, false) + } + + // looks like it's up to us to do all the work and renew the cert + wait = make(chan struct{}) + obtainCertWaitChans[name] = wait + obtainCertWaitChansMu.Unlock() + + // renew and reload the certificate + log.Printf("[INFO] Renewing certificate for %s", name) + err := cfg.RenewCert(name, false) + if err == nil { + // even though the recursive nature of the dynamic cert loading + // would just call this function anyway, we do it here to + // make the replacement as atomic as possible. + newCert, err := currentCert.configs[0].CacheManagedCertificate(name) + if err != nil { + log.Printf("[ERROR] loading renewed certificate for %s: %v", name, err) + } else { + // replace the old certificate with the new one + err = cfg.certCache.replaceCertificate(currentCert, newCert) + if err != nil { + log.Printf("[ERROR] Replacing certificate for %s: %v", name, err) + } + } + } + + // immediately unblock anyone waiting for it; doing this in + // a defer would risk deadlock because of the recursive call + // to getCertDuringHandshake below when we return! + obtainCertWaitChansMu.Lock() + close(wait) + delete(obtainCertWaitChans, name) + obtainCertWaitChansMu.Unlock() + + if err != nil { + return Certificate{}, err + } + + return cfg.getCertDuringHandshake(name, true, false) +} + +// tryDistributedChallengeSolver is to be called when the clientHello pertains to +// a TLS-ALPN challenge and a certificate is required to solve it. This method +// checks the distributed store of challenge info files and, if a matching ServerName +// is present, it makes a certificate to solve this challenge and returns it. +// A boolean true is returned if a valid certificate is returned. +func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInfo) (Certificate, bool, error) { + tokenKey := distributedSolver{}.challengeTokensKey(clientHello.ServerName) + chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey) + if err != nil { + if _, ok := err.(ErrNotExist); ok { + return Certificate{}, false, nil + } + return Certificate{}, false, fmt.Errorf("opening distributed challenge token file %s: %v", tokenKey, err) + } + + var chalInfo challengeInfo + err = json.Unmarshal(chalInfoBytes, &chalInfo) + if err != nil { + return Certificate{}, false, fmt.Errorf("decoding challenge token file %s (corrupted?): %v", tokenKey, err) + } + + cert, err := acme.TLSALPNChallengeCert(chalInfo.Domain, chalInfo.KeyAuth) + if err != nil { + return Certificate{}, false, fmt.Errorf("making TLS-ALPN challenge certificate: %v", err) + } + if cert == nil { + return Certificate{}, false, fmt.Errorf("got nil TLS-ALPN challenge certificate but no error") + } + + return Certificate{Certificate: *cert}, true, nil +} + +// obtainCertWaitChans is used to coordinate obtaining certs for each hostname. +var obtainCertWaitChans = make(map[string]chan struct{}) +var obtainCertWaitChansMu sync.Mutex diff --git a/httphandler.go b/httphandler.go new file mode 100644 index 0000000..2fc03fc --- /dev/null +++ b/httphandler.go @@ -0,0 +1,111 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/xenolf/lego/acme" +) + +// HTTPChallengeHandler wraps h in a handler that can solve the ACME +// HTTP challenge. cfg is required, and it must have a certificate +// cache backed by a functional storage facility, since that is where +// the challenge state is stored between initiation and solution. +// +// If a request is not an ACME HTTP challenge, h willl be invoked. +func (cfg *Config) HTTPChallengeHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if cfg.HandleHTTPChallenge(w, r) { + return + } + h.ServeHTTP(w, r) + }) +} + +// HandleHTTPChallenge uses cfg to solve challenge requests from an ACME +// server that were initiated by this instance or any other instance in +// this cluster (being, any instances using the same storage cfg does). +// +// If the HTTP challenge is disabled, this function is a no-op. +// +// If cfg is nil or if cfg does not have a certificate cache backed by +// usable storage, solving the HTTP challenge will fail. +// +// It returns true if it handled the request; if so, the response has +// already been written. If false is returned, this call was a no-op and +// the request has not been handled. +func (cfg *Config) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { + if cfg == nil { + return false + } + if cfg.DisableHTTPChallenge { + return false + } + if !strings.HasPrefix(r.URL.Path, challengeBasePath) { + return false + } + return cfg.distributedHTTPChallengeSolver(w, r) +} + +// distributedHTTPChallengeSolver checks to see if this challenge +// request was initiated by this or another instance which uses the +// same storage as cfg does, and attempts to complete the challenge for +// it. It returns true if the request was handled; false otherwise. +func (cfg *Config) distributedHTTPChallengeSolver(w http.ResponseWriter, r *http.Request) bool { + if cfg == nil { + return false + } + + tokenKey := distributedSolver{}.challengeTokensKey(r.Host) + chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey) + if err != nil { + if _, ok := err.(ErrNotExist); !ok { + log.Printf("[ERROR][%s] Opening distributed HTTP challenge token file: %v", r.Host, err) + } + return false + } + + var chalInfo challengeInfo + err = json.Unmarshal(chalInfoBytes, &chalInfo) + if err != nil { + log.Printf("[ERROR][%s] Decoding challenge token file %s (corrupted?): %v", r.Host, tokenKey, err) + return false + } + + return answerHTTPChallenge(w, r, chalInfo) +} + +// answerHTTPChallenge solves the challenge with chalInfo. +// Most of this code borrowed from xenolf/lego's built-in HTTP-01 +// challenge solver in March 2018. +func answerHTTPChallenge(w http.ResponseWriter, r *http.Request, chalInfo challengeInfo) bool { + challengeReqPath := acme.HTTP01ChallengePath(chalInfo.Token) + if r.URL.Path == challengeReqPath && + strings.HasPrefix(r.Host, chalInfo.Domain) && + r.Method == "GET" { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte(chalInfo.KeyAuth)) + r.Close = true + log.Printf("[INFO][%s] Served key authentication (distributed)", chalInfo.Domain) + return true + } + return false +} + +const challengeBasePath = "/.well-known/acme-challenge" diff --git a/maintain.go b/maintain.go new file mode 100644 index 0000000..88004a3 --- /dev/null +++ b/maintain.go @@ -0,0 +1,305 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "log" + "time" + + "golang.org/x/crypto/ocsp" +) + +// maintainAssets is a permanently-blocking function +// that loops indefinitely and, on a regular schedule, checks +// certificates for expiration and initiates a renewal of certs +// that are expiring soon. It also updates OCSP stapling and +// performs other maintenance of assets. It should only be +// called once per process. +// +// You must pass in the channel which you'll close when +// maintenance should stop, to allow this goroutine to clean up +// after itself and unblock. (Not that you HAVE to stop it...) +func (certCache *Cache) maintainAssets() { + renewalTicker := time.NewTicker(certCache.RenewInterval) + ocspTicker := time.NewTicker(certCache.OCSPInterval) + + for { + select { + case <-renewalTicker.C: + log.Println("[INFO] Scanning for expiring certificates") + err := certCache.RenewManagedCertificates(false) + if err != nil { + log.Printf("[ERROR] Renewing managed certificates: %v", err) + } + log.Println("[INFO] Done checking certificates") + case <-ocspTicker.C: + log.Println("[INFO] Scanning for stale OCSP staples") + certCache.UpdateOCSPStaples() + certCache.DeleteOldStapleFiles() + log.Println("[INFO] Done checking OCSP staples") + case <-certCache.stopChan: + renewalTicker.Stop() + ocspTicker.Stop() + log.Println("[INFO] Stopped certificate maintenance routine") + return + } + } +} + +// RenewManagedCertificates renews managed certificates, +// including ones loaded on-demand. +func (certCache *Cache) RenewManagedCertificates(interactive bool) error { + // we use the queues for a very important reason: to do any and all + // operations that could require an exclusive write lock outside + // of the read lock! otherwise we get a deadlock, yikes. in other + // words, our first iteration through the certificate cache does NOT + // perform any operations--only queues them--so that more fine-grained + // write locks may be obtained during the actual operations. + var renewQueue, reloadQueue, deleteQueue []Certificate + + certCache.mu.RLock() + for certKey, cert := range certCache.cache { + if len(cert.configs) == 0 { + // this is bad if this happens, probably a programmer error (oops) + log.Printf("[ERROR] No associated TLS config for certificate with names %v; unable to manage", cert.Names) + continue + } + if !cert.managed { + continue + } + + // the list of names on this cert should never be empty... programmer error? + if cert.Names == nil || len(cert.Names) == 0 { + log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v - removing from cache", certKey, cert.Names) + deleteQueue = append(deleteQueue, cert) + continue + } + + // if time is up or expires soon, we need to try to renew it + if cert.NeedsRenewal() { + // see if the certificate in storage has already been renewed, possibly by another + // instance that didn't coordinate with this one; if so, just load it (this + // might happen if another instance already renewed it - kinda sloppy but checking disk + // first is a simple way to possibly drastically reduce rate limit problems) + storedCertExpiring, err := managedCertInStorageExpiresSoon(cert) + if err != nil { + // hmm, weird, but not a big deal, maybe it was deleted or something + log.Printf("[NOTICE] Error while checking if certificate for %v in storage is also expiring soon: %v", + cert.Names, err) + } else if !storedCertExpiring { + // if the certificate is NOT expiring soon and there was no error, then we + // are good to just reload the certificate from storage instead of repeating + // a likely-unnecessary renewal procedure + reloadQueue = append(reloadQueue, cert) + continue + } + + // the certificate in storage has not been renewed yet, so we will do it + // NOTE: It is super-important to note that the TLS-ALPN challenge requires + // a write lock on the cache in order to complete its challenge, so it is extra + // vital that this renew operation does not happen inside our read lock! + renewQueue = append(renewQueue, cert) + } + } + certCache.mu.RUnlock() + + // Reload certificates that merely need to be updated in memory + for _, oldCert := range reloadQueue { + timeLeft := oldCert.NotAfter.Sub(time.Now().UTC()) + log.Printf("[INFO] Certificate for %v expires in %v, but is already renewed in storage; reloading stored certificate", + oldCert.Names, timeLeft) + + err := certCache.reloadManagedCertificate(oldCert) + if err != nil { + if interactive { + return err // operator is present, so report error immediately + } + log.Printf("[ERROR] Loading renewed certificate: %v", err) + } + } + + // Renewal queue + for _, oldCert := range renewQueue { + timeLeft := oldCert.NotAfter.Sub(time.Now().UTC()) + log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", oldCert.Names, timeLeft) + + // Get the name which we should use to renew this certificate; + // we only support managing certificates with one name per cert, + // so this should be easy. + renewName := oldCert.Names[0] + + // perform renewal + err := oldCert.configs[0].RenewCert(renewName, interactive) + if err != nil { + if interactive { + // Certificate renewal failed and the operator is present. See a discussion about + // this in issue mholt/caddy#642. For a while, we only stopped if the certificate + // was expired, but in reality, there is no difference between reporting it now + // versus later, except that there's somebody present to deal withit right now. + // Follow-up: See issue mholt/caddy#1680. Only fail in this case if the certificate + // is dangerously close to expiration. + timeLeft := oldCert.NotAfter.Sub(time.Now().UTC()) + if timeLeft < oldCert.configs[0].RenewDurationBeforeAtStartup { + return err + } + } + log.Printf("[ERROR] %v", err) + if oldCert.configs[0].OnDemand != nil { + // loaded dynamically, remove dynamically + deleteQueue = append(deleteQueue, oldCert) + } + continue + } + + // successful renewal, so update in-memory cache by loading + // renewed certificate so it will be used with handshakes + err = certCache.reloadManagedCertificate(oldCert) + if err != nil { + if interactive { + return err // operator is present, so report error immediately + } + log.Printf("[ERROR] %v", err) + } + } + + // Deletion queue + for _, cert := range deleteQueue { + certCache.mu.Lock() + // remove any pointers to this certificate from Configs + for _, cfg := range cert.configs { + for name, certKey := range cfg.certificates { + if certKey == cert.Hash { + delete(cfg.certificates, name) + } + } + } + // then delete the certificate from the cache + delete(certCache.cache, cert.Hash) + certCache.mu.Unlock() + } + + return nil +} + +// UpdateOCSPStaples updates the OCSP stapling in all +// eligible, cached certificates. +// +// OCSP maintenance strives to abide the relevant points on +// Ryan Sleevi's recommendations for good OCSP support: +// https://gist.github.com/sleevi/5efe9ef98961ecfb4da8 +func (certCache *Cache) UpdateOCSPStaples() { + // Create a temporary place to store updates + // until we release the potentially long-lived + // read lock and use a short-lived write lock + // on the certificate cache. + type ocspUpdate struct { + rawBytes []byte + parsed *ocsp.Response + } + updated := make(map[string]ocspUpdate) + + certCache.mu.RLock() + for certHash, cert := range certCache.cache { + // no point in updating OCSP for expired certificates + if time.Now().After(cert.NotAfter) { + continue + } + + var lastNextUpdate time.Time + if cert.OCSP != nil { + lastNextUpdate = cert.OCSP.NextUpdate + if freshOCSP(cert.OCSP) { + continue // no need to update staple if ours is still fresh + } + } + + err := certCache.stapleOCSP(&cert, nil) + if err != nil { + if cert.OCSP != nil { + // if there was no staple before, that's fine; otherwise we should log the error + log.Printf("[ERROR] Checking OCSP: %v", err) + } + continue + } + + // By this point, we've obtained the latest OCSP response. + // If there was no staple before, or if the response is updated, make + // sure we apply the update to all names on the certificate. + if cert.OCSP != nil && (lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate) { + log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s", + cert.Names, lastNextUpdate, cert.OCSP.NextUpdate) + updated[certHash] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP} + } + } + certCache.mu.RUnlock() + + // These write locks should be brief since we have all the info we need now. + for certKey, update := range updated { + certCache.mu.Lock() + cert := certCache.cache[certKey] + cert.OCSP = update.parsed + cert.Certificate.OCSPStaple = update.rawBytes + certCache.cache[certKey] = cert + certCache.mu.Unlock() + } +} + +// DeleteOldStapleFiles deletes cached OCSP staples that have expired. +// TODO: We should do this for long-expired certificates, too. +func (certCache *Cache) DeleteOldStapleFiles() { + ocspKeys, err := certCache.storage.List(prefixOCSP) + if err != nil { + // maybe just hasn't been created yet; no big deal + return + } + for _, key := range ocspKeys { + ocspBytes, err := certCache.storage.Load(key) + if err != nil { + log.Printf("[ERROR] While deleting old OCSP staples, unable to load staple file: %v", err) + continue + } + resp, err := ocsp.ParseResponse(ocspBytes, nil) + if err != nil { + // contents are invalid; delete it + err = certCache.storage.Delete(key) + if err != nil { + log.Printf("[ERROR] Purging corrupt staple file %s: %v", key, err) + } + continue + } + if time.Now().After(resp.NextUpdate) { + // response has expired; delete it + err = certCache.storage.Delete(key) + if err != nil { + log.Printf("[ERROR] Purging expired staple file %s: %v", key, err) + } + } + } +} + +const ( + // DefaultRenewInterval is how often to check certificates for renewal. + DefaultRenewInterval = 12 * time.Hour + + // DefaultRenewDurationBefore is how long before expiration to renew certificates. + DefaultRenewDurationBefore = (24 * time.Hour) * 30 + + // DefaultRenewDurationBeforeAtStartup is how long before expiration to require + // a renewed certificate when the process is first starting up (see mholt/caddy#1680). + DefaultRenewDurationBeforeAtStartup = (24 * time.Hour) * 7 + + // DefaultOCSPInterval is how often to check if OCSP stapling needs updating. + DefaultOCSPInterval = 1 * time.Hour +) diff --git a/memorysync.go b/memorysync.go new file mode 100644 index 0000000..52884ed --- /dev/null +++ b/memorysync.go @@ -0,0 +1,85 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "fmt" + "sync" +) + +// MemoryLocker implements the Locker interface +// using memory. An empty value is NOT VALID, +// so you must use NewMemoryLocker() to get one. +type MemoryLocker struct { + nameLocks map[string]*MemoryWaiter + nameLocksMu *sync.Mutex +} + +// NewMemoryLocker returns a valid Locker backed by fs. +func NewMemoryLocker() *MemoryLocker { + return &MemoryLocker{ + nameLocks: make(map[string]*MemoryWaiter), + nameLocksMu: new(sync.Mutex), + } +} + +// TryLock attempts to get a lock for name, otherwise it returns +// a Waiter value to wait until the other process is finished. +func (l *MemoryLocker) TryLock(name string) (Waiter, error) { + l.nameLocksMu.Lock() + defer l.nameLocksMu.Unlock() + + // see if lock already exists within this process + w, ok := l.nameLocks[name] + if ok { + return w, nil + } + + // we got the lock, so create it + w = &MemoryWaiter{wg: new(sync.WaitGroup)} + w.wg.Add(1) + l.nameLocks[name] = w + + return nil, nil +} + +// Unlock releases the lock for name. +func (l *MemoryLocker) Unlock(name string) error { + l.nameLocksMu.Lock() + defer l.nameLocksMu.Unlock() + + w, ok := l.nameLocks[name] + if !ok { + return fmt.Errorf("MemoryLocker: no lock to release for %s", name) + } + + w.wg.Done() + delete(l.nameLocks, name) + + return nil +} + +// MemoryWaiter implements Waiter in memory. +type MemoryWaiter struct { + wg *sync.WaitGroup +} + +// Wait waits until w.wg is done. +func (w *MemoryWaiter) Wait() { + w.Wait() +} + +var _ Locker = &MemoryLocker{} +var _ Waiter = &MemoryWaiter{} diff --git a/ocsp.go b/ocsp.go new file mode 100644 index 0000000..5ee13fd --- /dev/null +++ b/ocsp.go @@ -0,0 +1,209 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "time" + + "golang.org/x/crypto/ocsp" +) + +// stapleOCSP staples OCSP information to cert for hostname name. +// If you have it handy, you should pass in the PEM-encoded certificate +// bundle; otherwise the DER-encoded cert will have to be PEM-encoded. +// If you don't have the PEM blocks already, just pass in nil. +// +// Errors here are not necessarily fatal, it could just be that the +// certificate doesn't have an issuer URL. +func (certCache *Cache) stapleOCSP(cert *Certificate, pemBundle []byte) error { + if pemBundle == nil { + // we need a PEM encoding only for some function calls below + bundle := new(bytes.Buffer) + for _, derBytes := range cert.Certificate.Certificate { + pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + } + pemBundle = bundle.Bytes() + } + + var ocspBytes []byte + var ocspResp *ocsp.Response + var ocspErr error + var gotNewOCSP bool + + // First try to load OCSP staple from storage and see if + // we can still use it. + ocspStapleKey := prefixOCSPStaple(cert, pemBundle) + cachedOCSP, err := certCache.storage.Load(ocspStapleKey) + if err == nil { + resp, err := ocsp.ParseResponse(cachedOCSP, nil) + if err == nil { + if freshOCSP(resp) { + // staple is still fresh; use it + ocspBytes = cachedOCSP + ocspResp = resp + } + } else { + // invalid contents; delete the file + // (we do this independently of the maintenance routine because + // in this case we know for sure this should be a staple file + // because we loaded it by name, whereas the maintenance routine + // just iterates the list of files, even if somehow a non-staple + // file gets in the folder. in this case we are sure it is corrupt.) + err := certCache.storage.Delete(ocspStapleKey) + if err != nil { + log.Printf("[WARNING] Unable to delete invalid OCSP staple file: %v", err) + } + } + } + + // If we couldn't get a fresh staple by reading the cache, + // then we need to request it from the OCSP responder + if ocspResp == nil || len(ocspBytes) == 0 { + ocspBytes, ocspResp, ocspErr = getOCSPForCert(pemBundle) + if ocspErr != nil { + // An error here is not a problem because a certificate may simply + // not contain a link to an OCSP server. But we should log it anyway. + // There's nothing else we can do to get OCSP for this certificate, + // so we can return here with the error. + return fmt.Errorf("no OCSP stapling for %v: %v", cert.Names, ocspErr) + } + gotNewOCSP = true + } + + // By now, we should have a response. If good, staple it to + // the certificate. If the OCSP response was not loaded from + // storage, we persist it for next time. + if ocspResp.Status == ocsp.Good { + if ocspResp.NextUpdate.After(cert.NotAfter) { + // uh oh, this OCSP response expires AFTER the certificate does, that's kinda bogus. + // it was the reason a lot of Symantec-validated sites (not Caddy) went down + // in October 2017. https://twitter.com/mattiasgeniar/status/919432824708648961 + return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)", + cert.Names, cert.NotAfter.Sub(ocspResp.NextUpdate)) + } + cert.Certificate.OCSPStaple = ocspBytes + cert.OCSP = ocspResp + if gotNewOCSP { + err := certCache.storage.Store(ocspStapleKey, ocspBytes) + if err != nil { + return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err) + } + } + } + + return nil +} + +// getOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response, +// the parsed response, and an error, if any. The returned []byte can be passed directly +// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the +// issued certificate, this function will try to get the issuer certificate from the +// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return +// values are nil, the OCSP status may be assumed OCSPUnknown. +// +// Borrowed from github.com/xenolf/lego +func getOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { + // TODO: Perhaps this should be synchronized too, with a Locker? + + certificates, err := parseCertsFromPEMBundle(bundle) + if err != nil { + return nil, nil, err + } + + // We expect the certificate slice to be ordered downwards the chain. + // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, + // which should always be the first two certificates. If there's no + // OCSP server listed in the leaf cert, there's nothing to do. And if + // we have only one certificate so far, we need to get the issuer cert. + issuedCert := certificates[0] + if len(issuedCert.OCSPServer) == 0 { + return nil, nil, fmt.Errorf("no OCSP server specified in certificate") + } + if len(certificates) == 1 { + if len(issuedCert.IssuingCertificateURL) == 0 { + return nil, nil, fmt.Errorf("no URL to issuing certificate") + } + + resp, err := http.Get(issuedCert.IssuingCertificateURL[0]) + if err != nil { + return nil, nil, fmt.Errorf("getting issuer certificate: %v", err) + } + defer resp.Body.Close() + + issuerBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*1024)) + if err != nil { + return nil, nil, fmt.Errorf("reading issuer certificate: %v", err) + } + + issuerCert, err := x509.ParseCertificate(issuerBytes) + if err != nil { + return nil, nil, fmt.Errorf("parsing issuer certificate: %v", err) + } + + // insert it into the slice on position 0; + // we want it ordered right SRV CRT -> CA + certificates = append(certificates, issuerCert) + } + + issuerCert := certificates[1] + + ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) + if err != nil { + return nil, nil, fmt.Errorf("creating OCSP request: %v", err) + } + + reader := bytes.NewReader(ocspReq) + req, err := http.Post(issuedCert.OCSPServer[0], "application/ocsp-request", reader) + if err != nil { + return nil, nil, fmt.Errorf("making OCSP request: %v", err) + } + defer req.Body.Close() + + ocspResBytes, err := ioutil.ReadAll(io.LimitReader(req.Body, 1024*1024)) + if err != nil { + return nil, nil, fmt.Errorf("reading OCSP response: %v", err) + } + + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) + if err != nil { + return nil, nil, fmt.Errorf("parsing OCSP response: %v", err) + } + + return ocspResBytes, ocspRes, nil +} + +// freshOCSP returns true if resp is still fresh, +// meaning that it is not expedient to get an +// updated response from the OCSP server. +func freshOCSP(resp *ocsp.Response) bool { + nextUpdate := resp.NextUpdate + // If there is an OCSP responder certificate, and it expires before the + // OCSP response, use its expiration date as the end of the OCSP + // response's validity period. + if resp.Certificate != nil && resp.Certificate.NotAfter.Before(nextUpdate) { + nextUpdate = resp.Certificate.NotAfter + } + // start checking OCSP staple about halfway through validity period for good measure + refreshTime := resp.ThisUpdate.Add(nextUpdate.Sub(resp.ThisUpdate) / 2) + return time.Now().Before(refreshTime) +} diff --git a/solvers.go b/solvers.go new file mode 100644 index 0000000..cac56a2 --- /dev/null +++ b/solvers.go @@ -0,0 +1,147 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "encoding/json" + "fmt" + "log" + "path/filepath" + + "github.com/xenolf/lego/acme" +) + +// tlsALPNSolver is a type that can solve TLS-ALPN challenges using +// an existing listener and our custom, in-memory certificate cache. +type tlsALPNSolver struct { + certCache *Cache +} + +// Present adds the challenge certificate to the cache. +func (s tlsALPNSolver) Present(domain, token, keyAuth string) error { + cert, err := acme.TLSALPNChallengeCert(domain, keyAuth) + if err != nil { + return err + } + certHash := hashCertificateChain(cert.Certificate) + s.certCache.mu.Lock() + s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{ + Certificate: *cert, + Names: []string{domain}, + Hash: certHash, // perhaps not necesssary + } + s.certCache.mu.Unlock() + return nil +} + +// CleanUp removes the challenge certificate from the cache. +func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error { + s.certCache.mu.Lock() + delete(s.certCache.cache, domain) + s.certCache.mu.Unlock() + return nil +} + +// tlsALPNCertKeyName returns the key to use when caching a cert +// for use with the TLS-ALPN ACME challenge. It is simply to help +// avoid conflicts (although at time of writing, there shouldn't +// be, since the cert cache is keyed by hash of certificate chain). +func tlsALPNCertKeyName(sniName string) string { + return sniName + ":acme-tls-alpn" +} + +// distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges +// to be solved by an instance other than the one which initiated it. +// This is useful behind load balancers or in other cluster/fleet +// configurations. The only requirement is that the instance which +// initiates the challenge shares the same storage and locker with +// the others in the cluster. The storage backing the certificate +// cache in distributedSolver.config is crucial. +// +// Obviously, the instance which completes the challenge must be +// serving on the HTTPChallengePort for the HTTP-01 challenge or the +// TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all +// the packets port-forwarded) to receive and handle the request. The +// server which receives the challenge must handle it by checking to +// see if the challenge token exists in storage, and if so, decode it +// and use it to serve up the correct response. HTTPChallengeHandler +// in this package as well as the GetCertificate method implemented +// by a Config support and even require this behavior. +// +// In short: the only two requirements for cluster operation are +// sharing sync and storage, and using the facilities provided by +// this package for solving the challenges. +type distributedSolver struct { + // The config with a certificate cache + // with a reference to the storage to + // use which is shared among all the + // instances in the cluster - REQUIRED. + config *Config + + // Since the distributedSolver is only a + // wrapper over an actual solver, place + // the actual solver here. + providerServer acme.ChallengeProvider +} + +// Present invokes the underlying solver's Present method +// and also stores domain, token, and keyAuth to the storage +// backing the certificate cache of dhs.config. +func (dhs distributedSolver) Present(domain, token, keyAuth string) error { + if dhs.providerServer != nil { + err := dhs.providerServer.Present(domain, token, keyAuth) + if err != nil { + return fmt.Errorf("presenting with standard provider server: %v", err) + } + } + + infoBytes, err := json.Marshal(challengeInfo{ + Domain: domain, + Token: token, + KeyAuth: keyAuth, + }) + if err != nil { + return err + } + + return dhs.config.certCache.storage.Store(dhs.challengeTokensKey(domain), infoBytes) +} + +// CleanUp invokes the underlying solver's CleanUp method +// and also cleans up any assets saved to storage. +func (dhs distributedSolver) CleanUp(domain, token, keyAuth string) error { + if dhs.providerServer != nil { + err := dhs.providerServer.CleanUp(domain, token, keyAuth) + if err != nil { + log.Printf("[ERROR] Cleaning up standard provider server: %v", err) + } + } + return dhs.config.certCache.storage.Delete(dhs.challengeTokensKey(domain)) +} + +// challengeTokensPrefix returns the key prefix for challenge info. +func (dhs distributedSolver) challengeTokensPrefix() string { + return filepath.Join(prefixCA(dhs.config.CA), "challenge_tokens") +} + +// challengeTokensKey returns the key to use to store and access +// challenge info for domain. +func (dhs distributedSolver) challengeTokensKey(domain string) string { + return filepath.Join(dhs.challengeTokensPrefix(), safeKey(domain)+".json") +} + +type challengeInfo struct { + Domain, Token, KeyAuth string +} diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..1d492c7 --- /dev/null +++ b/storage.go @@ -0,0 +1,212 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "net/url" + "path" + "regexp" + "strings" + "time" +) + +// Storage is a type that implements a key-value store. +// Keys are prefix-based, with forward slash '/' as separators +// and without a leading slash. +// +// Processes running in a cluster will wish to use the +// same Storage value (its implementation and configuration) +// in order to share certificates and other TLS resources +// with the cluster. +type Storage interface { + // Exists returns true if the key exists + // and there was no error checking. + Exists(key string) bool + + // Store puts value at key. + Store(key string, value []byte) error + + // Load retrieves the value at key. + Load(key string) ([]byte, error) + + // Delete deletes key. + Delete(key string) error + + // List returns all keys that match prefix. + List(prefix string) ([]string, error) + + // Stat returns information about key. + Stat(key string) (KeyInfo, error) +} + +// KeyInfo holds information about a key in storage. +type KeyInfo struct { + Key string + Modified time.Time + Size int64 +} + +// storeTx stores all the values or none at all. +func storeTx(s Storage, all []keyValue) error { + for i, kv := range all { + err := s.Store(kv.key, kv.value) + if err != nil { + for j := i - 1; j >= 0; j-- { + s.Delete(all[j].key) + } + return err + } + } + return nil +} + +// keyValue pairs a key and a value. +type keyValue struct { + key string + value []byte +} + +const ( + prefixACME = "acme" + prefixOCSP = "ocsp" +) + +func prefixCA(ca string) string { + caURL, err := url.Parse(ca) + if err != nil { + caURL = &url.URL{Host: ca} + } + return path.Join(prefixACME, safeKey(caURL.Host)) +} + +func prefixSite(ca, domain string) string { + return path.Join(prefixCA(ca), "sites", safeKey(domain)) +} + +// prefixSiteCert returns the path to the certificate file for domain. +func prefixSiteCert(ca, domain string) string { + return path.Join(prefixSite(ca, domain), safeKey(domain)+".crt") +} + +// prefixSiteKey returns the path to domain's private key file. +func prefixSiteKey(ca, domain string) string { + return path.Join(prefixSite(ca, domain), safeKey(domain)+".key") +} + +// prefixSiteMeta returns the path to the domain's asset metadata file. +func prefixSiteMeta(ca, domain string) string { + return path.Join(prefixSite(ca, domain), safeKey(domain)+".json") +} + +func prefixUsers(ca string) string { + return path.Join(prefixCA(ca), "users") +} + +// prefixUser gets the account folder for the user with email +func prefixUser(ca, email string) string { + if email == "" { + email = emptyEmail + } + return path.Join(prefixUsers(ca), safeKey(email)) +} + +// prefixUserReg gets the path to the registration file for the user with the +// given email address. +func prefixUserReg(ca, email string) string { + return safeUserKey(ca, email, "registration", ".json") +} + +// prefixUserKey gets the path to the private key file for the user with the +// given email address. +func prefixUserKey(ca, email string) string { + return safeUserKey(ca, email, "private", ".key") +} + +func prefixOCSPStaple(cert *Certificate, pemBundle []byte) string { + var ocspFileName string + if len(cert.Names) > 0 { + firstName := safeKey(cert.Names[0]) + ocspFileName = firstName + "-" + } + ocspFileName += fastHash(pemBundle) + return path.Join(prefixOCSP, ocspFileName) +} + +// safeUserKey returns a key for the given email, +// with the default filename, and the filename +// ending in the given extension. +func safeUserKey(ca, email, defaultFilename, extension string) string { + if email == "" { + email = emptyEmail + } + email = strings.ToLower(email) + filename := emailUsername(email) + if filename == "" { + filename = defaultFilename + } + filename = safeKey(filename) + return path.Join(prefixUser(ca, email), filename+extension) +} + +// emailUsername returns the username portion of an email address (part before +// '@') or the original input if it can't find the "@" symbol. +func emailUsername(email string) string { + at := strings.Index(email, "@") + if at == -1 { + return email + } else if at == 0 { + return email[1:] + } + return email[:at] +} + +// safeKey standardizes and sanitizes str for use in a file path. +func safeKey(str string) string { + str = strings.ToLower(str) + str = strings.TrimSpace(str) + + // replace a few specific characters + repl := strings.NewReplacer( + " ", "_", + "+", "_plus_", + "*", "wildcard_", + "..", "", // prevent directory traversal (regex allows single dots) + ) + str = repl.Replace(str) + + // finally remove all non-word characters + return safeKeyRE.ReplaceAllLiteralString(str, "") +} + +// safeKeyRE matches any undesirable characters in storage keys. +// Note that this allows dots, so you'll have to strip ".." manually. +var safeKeyRE = regexp.MustCompile(`[^\w@.-]`) + +// ErrNotExist is returned by Storage implementations when +// a resource is not found. It is similar to os.IsNotExist +// except this is a type, not a variable. +type ErrNotExist interface { + error +} + +// defaultFileStorage is a convenient, default storage +// implementation using the local file system. +var defaultFileStorage = FileStorage{Path: dataDir()} + +// DefaultStorage is the default Storage implementation. +var DefaultStorage Storage = defaultFileStorage + +// DefaultSync is a default sync to use. +var DefaultSync Locker diff --git a/user.go b/user.go new file mode 100644 index 0000000..d8d596c --- /dev/null +++ b/user.go @@ -0,0 +1,263 @@ +// Copyright 2015 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certmagic + +import ( + "bufio" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/xenolf/lego/acme" +) + +// User represents a Let's Encrypt user account. +type User struct { + Email string + Registration *acme.RegistrationResource + key crypto.PrivateKey +} + +// GetEmail gets u's email. +func (u User) GetEmail() string { + return u.Email +} + +// GetRegistration gets u's registration resource. +func (u User) GetRegistration() *acme.RegistrationResource { + return u.Registration +} + +// GetPrivateKey gets u's private key. +func (u User) GetPrivateKey() crypto.PrivateKey { + return u.key +} + +// newUser creates a new User for the given email address +// with a new private key. This function does NOT save the +// user to disk or register it via ACME. If you want to use +// a user account that might already exist, call getUser +// instead. It does NOT prompt the user. +func (cfg *Config) newUser(email string) (User, error) { + user := User{Email: email} + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return user, fmt.Errorf("generating private key: %v", err) + } + user.key = privateKey + return user, nil +} + +// getEmail does everything it can to obtain an email address +// from the user within the scope of memory and storage to use +// for ACME TLS. If it cannot get an email address, it returns +// empty string. (If user is present, it will warn the user of +// the consequences of an empty email.) This function MAY prompt +// the user for input. If userPresent is false, the operator +// will NOT be prompted and an empty email may be returned. +// If the user is prompted, a new User will be created and +// stored in storage according to the email address they +// provided (which might be blank). +func (cfg *Config) getEmail(userPresent bool) (string, error) { + // First try memory (command line flag or typed by user previously) + leEmail := cfg.Email + + // Then try to get most recent user email from storage + if leEmail == "" { + leEmail = cfg.mostRecentUserEmail() + cfg.Email = leEmail // save for next time + } + + // Looks like there is no email address readily available, + // so we will have to ask the user if we can. + if leEmail == "" && userPresent { + // evidently, no User data was present in storage; + // thus we must make a new User so that we can get + // the Terms of Service URL via our ACME client, phew! + user, err := cfg.newUser("") + if err != nil { + return "", err + } + + // get the agreement URL + agreementURL := agreementTestURL + if agreementURL == "" { + // we call acme.NewClient directly because newACMEClient + // would require that we already know the user's email + caURL := CA + if cfg.CA != "" { + caURL = cfg.CA + } + tempClient, err := acme.NewClient(caURL, user, "") + if err != nil { + return "", fmt.Errorf("making ACME client to get ToS URL: %v", err) + } + agreementURL = tempClient.GetToSURL() + } + + // prompt the user for an email address and terms agreement + reader := bufio.NewReader(stdin) + cfg.promptUserAgreement(agreementURL) + fmt.Println("Please enter your email address to signify agreement and to be notified") + fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.") + fmt.Print(" Email address: ") + leEmail, err = reader.ReadString('\n') + if err != nil && err != io.EOF { + return "", fmt.Errorf("reading email address: %v", err) + } + leEmail = strings.TrimSpace(leEmail) + cfg.Email = leEmail + cfg.Agreed = true + + // save the new user to preserve this for next time + user.Email = leEmail + err = cfg.saveUser(user) + if err != nil { + return "", err + } + } + + // lower-casing the email is important for consistency + return strings.ToLower(leEmail), nil +} + +// getUser loads the user with the given email from disk +// using the provided storage. If the user does not exist, +// it will create a new one, but it does NOT save new +// users to the disk or register them via ACME. It does +// NOT prompt the user. +func (cfg *Config) getUser(email string) (User, error) { + var user User + + regBytes, err := cfg.certCache.storage.Load(prefixUserReg(cfg.CA, email)) + if err != nil { + if _, ok := err.(ErrNotExist); ok { + // create a new user + return cfg.newUser(email) + } + return user, err + } + keyBytes, err := cfg.certCache.storage.Load(prefixUserKey(cfg.CA, email)) + if err != nil { + if _, ok := err.(ErrNotExist); ok { + // create a new user + return cfg.newUser(email) + } + return user, err + } + + err = json.Unmarshal(regBytes, &user) + if err != nil { + return user, err + } + user.key, err = decodePrivateKey(keyBytes) + return user, err +} + +// saveUser persists a user's key and account registration +// to the file system. It does NOT register the user via ACME +// or prompt the user. You must also pass in the storage +// wherein the user should be saved. It should be the storage +// for the CA with which user has an account. +func (cfg *Config) saveUser(user User) error { + regBytes, err := json.MarshalIndent(&user, "", "\t") + if err != nil { + return err + } + keyBytes, err := encodePrivateKey(user.key) + if err != nil { + return err + } + + all := []keyValue{ + { + key: prefixUserReg(cfg.CA, user.Email), + value: regBytes, + }, + { + key: prefixUserKey(cfg.CA, user.Email), + value: keyBytes, + }, + } + + return storeTx(cfg.certCache.storage, all) +} + +// promptUserAgreement simply outputs the standard user +// agreement prompt with the given agreement URL. +// It outputs a newline after the message. +func (cfg *Config) promptUserAgreement(agreementURL string) { + const userAgreementPrompt = `Your sites will be served over HTTPS automatically using Let's Encrypt. +By continuing, you agree to the Let's Encrypt Subscriber Agreement at:` + fmt.Printf("\n\n%s\n %s\n", userAgreementPrompt, agreementURL) +} + +// askUserAgreement prompts the user to agree to the agreement +// at the given agreement URL via stdin. It returns whether the +// user agreed or not. +func (cfg *Config) askUserAgreement(agreementURL string) bool { + cfg.promptUserAgreement(agreementURL) + fmt.Print("Do you agree to the terms? (y/n): ") + + reader := bufio.NewReader(stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return false + } + answer = strings.ToLower(strings.TrimSpace(answer)) + + return answer == "y" || answer == "yes" +} + +// mostRecentUserEmail finds the most recently-written user file +// in s. Since this is part of a complex sequence to get a user +// account, errors here are discarded to simplify code flow in +// the caller, and errors are not important here anyway. +func (cfg *Config) mostRecentUserEmail() string { + userList, err := cfg.certCache.storage.List(prefixUsers(cfg.CA)) + if err != nil || len(userList) == 0 { + return "" + } + sort.Slice(userList, func(i, j int) bool { + iInfo, _ := cfg.certCache.storage.Stat(prefixUser(cfg.CA, userList[i])) + jInfo, _ := cfg.certCache.storage.Stat(prefixUser(cfg.CA, userList[j])) + return iInfo.Modified.Before(jInfo.Modified) + }) + user, err := cfg.getUser(userList[0]) + if err != nil { + return "" + } + return user.Email +} + +// agreementTestURL is set during tests to skip requiring +// setting up an entire ACME CA endpoint. +var agreementTestURL string + +// stdin is used to read the user's input if prompted; +// this is changed by tests during tests. +var stdin = io.ReadWriter(os.Stdin) + +// The name of the folder for accounts where the email +// address was not provided; default 'username' if you will, +// but only for local/storage use, not with the CA. +const emptyEmail = "default"