diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index fb2cc2cb..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,131 +0,0 @@ -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/caddyserver/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/caddyserver/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/caddyserver/certmagic). This makes a copy of the code you can write to. - -2. If you don't already have this repo (caddyserver/certmagic.git) repo on your computer, get it with `go get github.com/caddyserver/certmagic`. - -3. Tell git that it can push the caddyserver/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 caddyserver/certmagic.git repo on your computer. - -5. Push your changes to your fork: `git push myfork` - -6. [Create a pull request](https://github.com/caddyserver/certmagic/pull/new/master) to merge your changes into caddyserver/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/caddyserver/certmagic/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/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/caddyserver/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/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4703554b..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b9965f67..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Bug report -about: For behaviors which violate documentation or cause incorrect results -title: '' -labels: '' -assignees: '' - ---- - - - -## What version of the package are you using? - - - -## What are you trying to do? - - - -## What steps did you take? - - - -## What did you expect to happen, and what actually happened instead? - - - -## How do you think this should be fixed? - - - -## Please link to any related issues, pull requests, and/or discussion - - - -## Bonus: What do you use CertMagic for, and do you find it useful? - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index d5b48330..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: feature request -assignees: '' - ---- - - - -## What would you like to have changed? - - - -## Why is this feature a useful, necessary, and/or important addition to this project? - - - -## What alternatives are there, or what are you doing in the meantime to work around the lack of this feature? - - - -## Please link to any relevant issues, pull requests, or other discussions. - diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 0bdc9ed5..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Question -about: For help or questions about this package -title: '' -labels: question -assignees: '' - ---- - - - -## What is your question? - - - -## What have you already tried? - - - -## Include any other information or discussion. - - - -## Bonus: What do you use this package for, and does it help you? - diff --git a/.github/update_dependencies.sh b/.github/update_dependencies.sh new file mode 100755 index 00000000..e6e2770f --- /dev/null +++ b/.github/update_dependencies.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +PROJECTS=$(dirname "$0")/../.. + +go get -x github.com/sagernet/sing-dns@$(git -C $PROJECTS/sing-dns rev-parse HEAD) +go mod tidy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8cb1315c..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,66 +0,0 @@ -# Used as inspiration: https://github.com/mvdan/github-actions-golang - -name: Tests - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - test: - strategy: - matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] - go: [ '1.18' ] - - runs-on: ${{ matrix.os }} - - steps: - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go }} - - - name: Checkout code - uses: actions/checkout@v2 - - - name: Print Go version and environment - id: vars - run: | - printf "Using go at: $(which go)\n" - printf "Go version: $(go version)\n" - printf "\n\nGo environment:\n\n" - go env - printf "\n\nSystem environment:\n\n" - env - # Calculate the short SHA1 hash of the git commit - echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" - - - name: Cache the build cache - uses: actions/cache@v2 - with: - # In order: - # * Module download cache - # * Build cache (Linux) - # * Build cache (Mac) - # * Build cache (Windows) - path: | - ~/go/pkg/mod - ~/.cache/go-build - ~/Library/Caches/go-build - ~\AppData\Local\go-build - key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.go }}-go-ci - - - name: Get dependencies - run: | - go get -v -t -d ./... - - - name: Run tests - run: | - go test -v -coverprofile="cover-profile.out" -short -race ./... \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 6fb9a5f6..00000000 --- a/README.md +++ /dev/null @@ -1,526 +0,0 @@ -

- CertMagic -

-

Easy and Powerful TLS Automation

-

The same library used by the Caddy Web Server

-

- - - -

- - -Caddy's [automagic TLS features](https://caddyserver.com/docs/automatic-https)—now for your own Go programs—in one powerful and easy-to-use library! - -CertMagic is the most mature, robust, and powerful ACME client integration for Go... and perhaps ever. - -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([]string{"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 matches CertMagic's maturity and reliability. - - - - -CertMagic - Automatic HTTPS using Let's Encrypt -=============================================== - -## 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) - - [Rate limiting](#rate-limiting) - - [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-tlsconfig) - - [Advanced use](#advanced-use) - - [Wildcard Certificates](#wildcard-certificates) - - [Behind a load balancer (or in a cluster)](#behind-a-load-balancer-or-in-a-cluster) - - [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 -- Solves all 3 ACME challenges: HTTP, TLS-ALPN, and DNS -- Most robust error handling of _any_ ACME client - - Challenges are randomized to avoid accidental dependence - - Challenges are rotated to overcome certain network blockages - - Robust retries for up to 30 days - - Exponential backoff with carefully-tuned intervals - - Retries with optional test/staging CA endpoint instead of production, to avoid rate limits -- Written in Go, a language with memory-safety guarantees -- Powered by [ACMEz](https://github.com/mholt/acmez), _the_ premier ACME client library for Go -- All [libdns](https://github.com/libdns) DNS providers work out-of-the-box -- Pluggable storage implementations (default: file system) -- Wildcard certificates -- Automatic OCSP stapling ([done right](https://gist.github.com/sleevi/5efe9ef98961ecfb4da8#gistcomment-2336055)) [keeps your sites online!](https://twitter.com/caddyserver/status/1234874273724084226) - - Will [automatically attempt](https://twitter.com/mholt6/status/1235577699541762048) to replace [revoked certificates](https://community.letsencrypt.org/t/2020-02-29-caa-rechecking-bug/114591/3?u=mholt)! - - Staples stored to disk in case of responder outages -- Distributed solving of all challenges (works behind load balancers) - - Highly efficient, coordinated management in a fleet - - Active locking - - Smart queueing -- Supports "on-demand" issuance of certificates (during TLS handshakes!) - - Caddy / CertMagic pioneered this technology - - Custom decision functions to regulate and throttle on-demand behavior -- Optional event hooks for observation -- 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 to hundreds of thousands of names/certificates per instance -- Use in conjunction with your own certificates - - -## Requirements - -0. ACME server (can be a publicly-trusted CA, or your own) -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 github.com/caddyserver/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/uninitialized `Config` is _not_ a valid one! In time, you will learn to use the force of `certmagic.NewDefault()` as I have. - -#### Defaults - -The default `Config` value is called `certmagic.Default`. Change its fields to suit your needs, then call `certmagic.NewDefault()` when you need a valid `Config` value. In other words, `certmagic.Default` is a template and is not valid for use directly. - -You can set the default values easily, for example: `certmagic.Default.Issuer = ...`. - -Similarly, to configure ACME-specific defaults, use `certmagic.DefaultACME`. - -The high-level functions in this package (`HTTPS()`, `Listen()`, `ManageSync()`, and `ManageAsync()`) use the default config exclusively. This is how most of you will interact with the package. This is suitable when all your certificates are managed the same way. However, if you need to manage certificates differently depending on their name, you will need to make your own cache and configs (keep reading). - - -#### 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.DefaultACME.Email` or always setting the `Email` field of a new `Config` struct. - - -#### Rate limiting - -To avoid firehosing the CA's servers, CertMagic has built-in rate limiting. Currently, its default limit is up to 10 transactions (obtain or renew) every 1 minute (sliding window). This can be changed by setting the `RateLimitEvents` and `RateLimitEventsWindow` variables, if desired. - -The CA may still enforce their own rate limits, and there's nothing (well, nothing ethical) CertMagic can do to bypass them for you. - -Additionally, CertMagic will retry failed validations with exponential backoff for up to 30 days, with a reasonable maximum interval between attempts (an "attempt" means trying each enabled challenge type once). - - -### 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.DefaultACME.CA = certmagic.LetsEncryptStagingCA` or set `CA` of every `ACMEIssuer` struct. - - - -### Examples - -There are many ways to use this library. We'll start with the highest-level (simplest) and work down (more control). - -All these high-level examples use `certmagic.Default` and `certmagic.DefaultACME` for the config and the default cache and storage for serving up certificates. - -First, we'll follow best practices and do the following: - -```go -// read and agree to your CA's legal documents -certmagic.DefaultACME.Agreed = true - -// provide an email address -certmagic.DefaultACME.Email = "you@yours.com" - -// use the staging endpoint while we're developing -certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA -``` - -For fully-functional program examples, check out [this Twitter thread](https://twitter.com/mholt6/status/1073103805112147968) (or read it [unrolled into a single post](https://threadreaderapp.com/thread/1073103805112147968.html)). (Note that the package API has changed slightly since these posts.) - - -#### Serving HTTP handlers with HTTPS - -```go -err := certmagic.HTTPS([]string{"example.com", "www.example.com"}, mux) -if err != nil { - return err -} -``` - -This starts HTTP and HTTPS listeners and redirects HTTP to HTTPS! - -#### 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 (particularly, if you need a different way of managing each certificate), you'll make and use a `Cache` and a `Config` like so: - -```go -cache := certmagic.NewCache(certmagic.CacheOptions{ - GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { - // do whatever you need to do to get the right - // configuration for this certificate; keep in - // mind that this config value is used as a - // template, and will be completed with any - // defaults that are set in the Default config - return &certmagic.Config{ - // ... - }, nil - }, - ... -}) - -magic := certmagic.New(cache, certmagic.Config{ - // any customizations you need go here -}) - -myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{ - CA: certmagic.LetsEncryptStagingCA, - Email: "you@yours.com", - Agreed: true, - // plus any other customizations you need -}) - -magic.Issuer = myACME - -// this obtains certificates or renews them if necessary -err := magic.ManageSync(context.TODO(), []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() - -// be sure to customize NextProtos if serving a specific -// application protocol after the TLS handshake, for example: -tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...) - -//// OR //// - -// 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, tlsalpn01.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 = myACME.HTTPChallengeHandler(httpMux) -``` - -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 `Default` config. - - -### Wildcard certificates - -At time of writing (December 2018), Let's Encrypt only issues wildcard certificates with the DNS challenge. You can easily enable the DNS challenge with CertMagic for numerous providers (see the relevant section in the docs). - - -### 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. That is the sole criteria for determining whether an instance is part of a cluster. - -The default Storage is 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 implementation, feel free to use one, provided that all the instances use the _same_ one. :) - -See [Storage](#storage) and the associated [pkg.go.dev](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Storage) for more information! - - -## The ACME Challenges - -This section describes how to solve the ACME challenges. Challenges are how you demonstrate to the certificate authority some control over your domain name, thus authorizing them to grant you a certificate for that name. [The great innovation of ACME](https://www.dotconferences.com/2016/10/matthew-holt-go-with-acme) is that verification by CAs can now be automated, rather than having to click links in emails (who ever thought that was a good idea??). - -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. - -Technically, only one challenge 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. And if one fails, any remaining enabled challenges are tried before giving up. - - -### 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.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "Lookit my cool website over HTTPS!") -}) - -http.ListenAndServe(":80", myACME.HTTPChallengeHandler(mux)) -``` - -If wrapping your handler is not a good solution, try this inside your `ServeHTTP()` instead: - -```go -magic := certmagic.NewDefault() -myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME) - -func ServeHTTP(w http.ResponseWriter, req *http.Request) { - if myACME.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, tlsalpn01.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 DNS providers with `libdns` implementations](https://github.com/libdns)! It always cleans up the temporary record after the challenge completes. - -To enable it, just set the `DNS01Solver` field on a `certmagic.ACMEIssuer` struct, or set the default `certmagic.ACMEIssuer.DNS01Solver` variable. For example, if my domains' DNS was served by Cloudflare: - -```go -import "github.com/libdns/cloudflare" - -certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{ - DNSProvider: &cloudflare.Provider{ - APIToken: "topsecret", - }, -} -``` - -Now the DNS challenge will be used by default, and I can obtain certificates for wildcard domains, too. Enabling the DNS challenge disables the other challenges for that `certmagic.ACMEIssuer` 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, or you don't want to manage all the certificates up front. 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 or specific 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, when you enable On-Demand issuance, you should set limits or policy to allow getting certificates. CertMagic has an implicit whitelist built-in which is sufficient for nearly everyone, but also has a more advanced way to control on-demand issuance. - -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.Default.OnDemand = new(certmagic.OnDemandConfig) -``` - -By setting this to a non-nil value, on-demand TLS is enabled for that config. For convenient security, CertMagic's high-level abstraction functions such as `HTTPS()`, `TLS()`, `ManageSync()`, `ManageAsync()`, and `Listen()` (which all accept a list of domain names) will whitelist those names automatically so only certificates for those names can be obtained when using the Default config. Usually this is sufficient for most users. - -However, if you require advanced control over which domains can be issued certificates on-demand (for example, if you do not know which domain names you are managing, or just need to defer their operations until later), you should implement your own DecisionFunc: - -```go -// if the decision function returns an error, a certificate -// may not be obtained for that name at that time -certmagic.Default.OnDemand = &certmagic.OnDemandConfig{ - DecisionFunc: func(name string) error { - if name != "example.com" { - return fmt.Errorf("not allowed") - } - return nil - }, -} -``` - -The [pkg.go.dev](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#OnDemandConfig) describes how to use this in full detail, so please check it out! - - -## 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_DATA_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 the same storage configuration. Sharing storage could be mounting a shared folder, or implementing some other distributed storage system such as a database server or KV store. - -The easiest way to change the storage being used is to set `certmagic.Default.Storage` to a value that satisfies the [Storage interface](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Storage). Keep in mind that a valid `Storage` must be able to implement some operations atomically in order to provide locking and synchronization. - -If you write a Storage implementation, please add it to the [project wiki](https://github.com/caddyserver/certmagic/wiki/Storage-Implementations) 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.Default.Storage` variable (before attempting to create any Configs) which defines how the cache is persisted. 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 relevant method on the `Config` to add your own certificate to the cache: - -- [`CacheUnmanagedCertificatePEMBytes()`](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Config.CacheUnmanagedCertificatePEMBytes) -- [`CacheUnmanagedCertificatePEMFile()`](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Config.CacheUnmanagedCertificatePEMFile) -- [`CacheUnmanagedTLSCertificate()`](https://pkg.go.dev/github.com/caddyserver/certmagic?tab=doc#Config.CacheUnmanagedTLSCertificate) - -Keep in mind that unmanaged certificates are (obviously) not renewed for you, so you'll have to replace them when you do. However, OCSP stapling is performed even for unmanaged certificates that qualify. - - -### 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 serving them, if you provide your own. - - -### How can I listen on ports 80 and 443? Do I have to run as root? - -On Linux, you can use `setcap` to grant your binary the permission to bind low ports: - -```bash -$ sudo setcap cap_net_bind_service=+ep /path/to/your/binary -``` - -and then you will not need to run with root privileges. - - -## Contributing - -We welcome your contributions! Please see our **[contributing guidelines](https://github.com/caddyserver/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 [ACMEz](https://github.com/mholt/acmez). CertMagic's code was originally a central part of 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. - -You can [watch a 2016 dotGo talk](https://www.dotconferences.com/2016/10/matthew-holt-go-with-acme) by the author of this library about using ACME to automate certificate management in Go programs: - -[![Matthew Holt speaking at dotGo 2016 about ACME in Go](https://user-images.githubusercontent.com/1128849/49921557-2d506780-fe6b-11e8-97bf-6053b6b4eb48.png)](https://www.dotconferences.com/2016/10/matthew-holt-go-with-acme) - - - -## 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/account_test.go b/account_test.go deleted file mode 100644 index 4fbd4048..00000000 --- a/account_test.go +++ /dev/null @@ -1,278 +0,0 @@ -// 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" - "context" - "os" - "path/filepath" - "reflect" - "strings" - "sync" - "testing" - "time" -) - -func TestNewAccount(t *testing.T) { - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - email := "me@foobar.com" - account, err := am.newAccount(email) - if err != nil { - t.Fatalf("Error creating account: %v", err) - } - if account.PrivateKey == nil { - t.Error("Private key is nil") - } - if account.Contact[0] != "mailto:"+email { - t.Errorf("Expected email to be %s, but was %s", email, account.Contact[0]) - } - if account.Status != "" { - t.Error("New account already has a status; it shouldn't") - } -} - -func TestSaveAccount(t *testing.T) { - ctx := context.Background() - - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata1_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - testStorageDir := testConfig.Storage.(*FileStorage).Path - defer func() { - err := os.RemoveAll(testStorageDir) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", testStorageDir, err) - } - }() - - email := "me@foobar.com" - account, err := am.newAccount(email) - if err != nil { - t.Fatalf("Error creating account: %v", err) - } - - err = am.saveAccount(ctx, am.CA, account) - if err != nil { - t.Fatalf("Error saving account: %v", err) - } - _, err = am.getAccount(ctx, am.CA, email) - if err != nil { - t.Errorf("Cannot access account data, error: %v", err) - } -} - -func TestGetAccountDoesNotAlreadyExist(t *testing.T) { - ctx := context.Background() - - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - account, err := am.getAccount(ctx, am.CA, "account_does_not_exist@foobar.com") - if err != nil { - t.Fatalf("Error getting account: %v", err) - } - - if account.PrivateKey == nil { - t.Error("Expected account to have a private key, but it was nil") - } -} - -func TestGetAccountAlreadyExists(t *testing.T) { - ctx := context.Background() - - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata2_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - testStorageDir := testConfig.Storage.(*FileStorage).Path - defer func() { - err := os.RemoveAll(testStorageDir) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", testStorageDir, err) - } - }() - - email := "me@foobar.com" - - // Set up test - account, err := am.newAccount(email) - if err != nil { - t.Fatalf("Error creating account: %v", err) - } - err = am.saveAccount(ctx, am.CA, account) - if err != nil { - t.Fatalf("Error saving account: %v", err) - } - - // Expect to load account from disk - loadedAccount, err := am.getAccount(ctx, am.CA, email) - if err != nil { - t.Fatalf("Error getting account: %v", err) - } - - // Assert keys are the same - if !privateKeysSame(account.PrivateKey, loadedAccount.PrivateKey) { - t.Error("Expected private key to be the same after loading, but it wasn't") - } - - // Assert emails are the same - if !reflect.DeepEqual(account.Contact, loadedAccount.Contact) { - t.Errorf("Expected contacts to be equal, but was '%s' before and '%s' after loading", account.Contact, loadedAccount.Contact) - } -} - -func TestGetEmailFromPackageDefault(t *testing.T) { - ctx := context.Background() - - DefaultACME.Email = "tEsT2@foo.com" - defer func() { - DefaultACME.Email = "" - discoveredEmail = "" - }() - - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata2_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - err := am.setEmail(ctx, true) - if err != nil { - t.Fatalf("getEmail error: %v", err) - } - lowerEmail := strings.ToLower(DefaultACME.Email) - if am.getEmail() != lowerEmail { - t.Errorf("Did not get correct email from memory; expected '%s' but got '%s'", lowerEmail, am.Email) - } -} - -func TestGetEmailFromUserInput(t *testing.T) { - ctx := context.Background() - - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata3_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - // let's not clutter up the output - origStdout := os.Stdout - os.Stdout = nil - agreementTestURL = "(none - testing)" - defer func() { - os.Stdout = origStdout - agreementTestURL = "" - }() - - email := "test3@foo.com" - stdin = bytes.NewBufferString(email + "\n") - err := am.setEmail(ctx, true) - if err != nil { - t.Fatalf("getEmail error: %v", err) - } - if am.email != email { - t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", email, am.Email) - } - if !am.isAgreed() { - t.Error("Expect Config.agreed to be true, but got false") - } -} - -func TestGetEmailFromRecent(t *testing.T) { - ctx := context.Background() - - am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata4_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - testStorageDir := testConfig.Storage.(*FileStorage).Path - defer func() { - err := os.RemoveAll(testStorageDir) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", testStorageDir, err) - } - }() - - DefaultACME.Email = "" - discoveredEmail = "" - - for i, eml := range []string{ - "test4-1@foo.com", - "test4-2@foo.com", - "TEST4-3@foo.com", // test case insensitivity - } { - account, err := am.newAccount(eml) - if err != nil { - t.Fatalf("Error creating user %d: %v", i, err) - } - err = am.saveAccount(ctx, am.CA, account) - if err != nil { - t.Fatalf("Error saving user %d: %v", i, err) - } - - // Change modified time so they're all different and the test becomes more deterministic - fs := testConfig.Storage.(*FileStorage) - userFolder := filepath.Join(fs.Path, am.storageKeyUserPrefix(am.CA, eml)) - f, err := os.Stat(userFolder) - if err != nil { - t.Fatalf("Could not access user folder for '%s': %v", eml, err) - } - chTime := f.ModTime().Add(time.Duration(i) * time.Hour) // 1 second isn't always enough spacing! - if err := os.Chtimes(userFolder, chTime, chTime); err != nil { - t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err) - } - } - err := am.setEmail(ctx, true) - if err != nil { - t.Fatalf("getEmail error: %v", err) - } - if am.getEmail() != "test4-3@foo.com" { - t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", am.Email) - } -} - -// agreementTestURL is set during tests to skip requiring -// setting up an entire ACME CA endpoint. -var agreementTestURL string diff --git a/acmeissuer_test.go b/acmeissuer_test.go deleted file mode 100644 index 9e0ee187..00000000 --- a/acmeissuer_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package certmagic - -const dummyCA = "https://example.com/acme/directory" diff --git a/cache_test.go b/cache_test.go deleted file mode 100644 index 7d95ba61..00000000 --- a/cache_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// 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 "testing" - -func TestNewCache(t *testing.T) { - noop := func(Certificate) (*Config, error) { return new(Config), nil } - c := NewCache(CacheOptions{GetConfigForCert: noop}) - defer c.Stop() - - if c.options.RenewCheckInterval != DefaultRenewCheckInterval { - t.Errorf("Expected RenewCheckInterval to be set to default value, but it wasn't: %s", c.options.RenewCheckInterval) - } - if c.options.OCSPCheckInterval != DefaultOCSPCheckInterval { - t.Errorf("Expected OCSPCheckInterval to be set to default value, but it wasn't: %s", c.options.OCSPCheckInterval) - } - if c.options.GetConfigForCert == nil { - t.Error("Expected GetConfigForCert to be set, but it was nil") - } - if c.cache == nil { - t.Error("Expected cache to be set, but it was nil") - } - if c.stopChan == nil { - t.Error("Expected stopChan to be set, but it was nil") - } -} diff --git a/certificates_test.go b/certificates_test.go deleted file mode 100644 index b5d45ea1..00000000 --- a/certificates_test.go +++ /dev/null @@ -1,222 +0,0 @@ -// 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" - "reflect" - "testing" - "time" -) - -func TestUnexportedGetCertificate(t *testing.T) { - certCache := &Cache{cache: make(map[string]Certificate), cacheIndex: make(map[string][]string)} - cfg := &Config{certCache: certCache} - - // When cache is empty - if _, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "example.com"}); matched || defaulted { - t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted) - } - - // When cache has one certificate in it - firstCert := Certificate{Names: []string{"example.com"}} - certCache.cache["0xdeadbeef"] = firstCert - certCache.cacheIndex["example.com"] = []string{"0xdeadbeef"} - if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "example.com"}); !matched || defaulted || cert.Names[0] != "example.com" { - t.Errorf("Didn't get a cert for 'example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) - } - - // When retrieving wildcard certificate - certCache.cache["0xb01dface"] = Certificate{Names: []string{"*.example.com"}} - certCache.cacheIndex["*.example.com"] = []string{"0xb01dface"} - if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "sub.example.com"}); !matched || defaulted || cert.Names[0] != "*.example.com" { - t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) - } - - // When no certificate matches and SNI is provided, return no certificate (should be TLS alert) - if cert, matched, defaulted := cfg.getCertificateFromCache(&tls.ClientHelloInfo{ServerName: "nomatch"}); matched || defaulted { - t.Errorf("Expected matched=false, defaulted=false; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert) - } -} - -func TestCacheCertificate(t *testing.T) { - certCache := &Cache{cache: make(map[string]Certificate), cacheIndex: make(map[string][]string)} - - certCache.cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}, hash: "foobar", Certificate: tls.Certificate{Leaf: &x509.Certificate{NotAfter: time.Now()}}}) - if len(certCache.cache) != 1 { - t.Errorf("Expected length of certificate cache to be 1") - } - if _, ok := certCache.cache["foobar"]; !ok { - t.Error("Expected first cert to be cached by key 'foobar', but it wasn't") - } - if _, ok := certCache.cacheIndex["example.com"]; !ok { - t.Error("Expected first cert to be keyed by 'example.com', but it wasn't") - } - if _, ok := certCache.cacheIndex["sub.example.com"]; !ok { - t.Error("Expected first cert to be keyed by 'sub.example.com', but it wasn't") - } - - // using same cache; and has cert with overlapping name, but different hash - certCache.cacheCertificate(Certificate{Names: []string{"example.com"}, hash: "barbaz", Certificate: tls.Certificate{Leaf: &x509.Certificate{NotAfter: time.Now()}}}) - if _, ok := certCache.cache["barbaz"]; !ok { - t.Error("Expected second cert to be cached by key 'barbaz.com', but it wasn't") - } - if hashes, ok := certCache.cacheIndex["example.com"]; !ok { - t.Error("Expected second cert to be keyed by 'example.com', but it wasn't") - } else if !reflect.DeepEqual(hashes, []string{"foobar", "barbaz"}) { - t.Errorf("Expected second cert to map to 'barbaz' but it was %v instead", hashes) - } -} - -func TestSubjectQualifiesForCert(t *testing.T) { - for i, test := range []struct { - host string - expect bool - }{ - {"hostname", true}, - {"example.com", true}, - {"sub.example.com", true}, - {"Sub.Example.COM", true}, - {"127.0.0.1", true}, - {"127.0.1.5", true}, - {"69.123.43.94", true}, - {"::1", true}, - {"::", true}, - {"0.0.0.0", true}, - {"", false}, - {" ", false}, - {"*.example.com", true}, - {"*.*.example.com", true}, - {"sub.*.example.com", false}, - {"*sub.example.com", false}, - {"**.tld", false}, - {"*", true}, - {"*.tld", true}, - {".tld", false}, - {"example.com.", false}, - {"localhost", true}, - {"foo.localhost", true}, - {"local", true}, - {"192.168.1.3", true}, - {"10.0.2.1", true}, - {"169.112.53.4", true}, - {"$hostname", false}, - {"%HOSTNAME%", false}, - {"{hostname}", false}, - {"hostname!", false}, - {"", false}, - {"# hostname", false}, - {"// hostname", false}, - {"user@hostname", false}, - {"hostname;", false}, - {`"hostname"`, false}, - } { - actual := SubjectQualifiesForCert(test.host) - if actual != test.expect { - t.Errorf("Test %d: Expected SubjectQualifiesForCert(%s)=%v, but got %v", - i, test.host, test.expect, actual) - } - } -} - -func TestSubjectQualifiesForPublicCert(t *testing.T) { - for i, test := range []struct { - host string - expect bool - }{ - {"hostname", true}, - {"example.com", true}, - {"sub.example.com", true}, - {"Sub.Example.COM", true}, - {"127.0.0.1", false}, - {"127.0.1.5", false}, - {"69.123.43.94", false}, - {"::1", false}, - {"::", false}, - {"0.0.0.0", false}, - {"", false}, - {" ", false}, - {"*.example.com", true}, - {"*.*.example.com", false}, - {"sub.*.example.com", false}, - {"*sub.example.com", false}, - {"*", false}, // won't be trusted by browsers - {"*.tld", false}, // won't be trusted by browsers - {".tld", false}, - {"example.com.", false}, - {"localhost", false}, - {"foo.localhost", false}, - {"local", true}, - {"foo.local", false}, - {"foo.bar.local", false}, - {"192.168.1.3", false}, - {"10.0.2.1", false}, - {"169.112.53.4", false}, - {"$hostname", false}, - {"%HOSTNAME%", false}, - {"{hostname}", false}, - {"hostname!", false}, - {"", false}, - {"# hostname", false}, - {"// hostname", false}, - {"user@hostname", false}, - {"hostname;", false}, - {`"hostname"`, false}, - } { - actual := SubjectQualifiesForPublicCert(test.host) - if actual != test.expect { - t.Errorf("Test %d: Expected SubjectQualifiesForPublicCert(%s)=%v, but got %v", - i, test.host, test.expect, actual) - } - } -} - -func TestMatchWildcard(t *testing.T) { - for i, test := range []struct { - subject, wildcard string - expect bool - }{ - {"hostname", "hostname", true}, - {"HOSTNAME", "hostname", true}, - {"hostname", "HOSTNAME", true}, - {"foo.localhost", "foo.localhost", true}, - {"foo.localhost", "bar.localhost", false}, - {"foo.localhost", "*.localhost", true}, - {"bar.localhost", "*.localhost", true}, - {"FOO.LocalHost", "*.localhost", true}, - {"Bar.localhost", "*.LOCALHOST", true}, - {"foo.bar.localhost", "*.localhost", false}, - {".localhost", "*.localhost", false}, - {"foo.localhost", "foo.*", false}, - {"foo.bar.local", "foo.*.local", false}, - {"foo.bar.local", "foo.bar.*", false}, - {"foo.bar.local", "*.bar.local", true}, - {"1.2.3.4.5.6", "*.2.3.4.5.6", true}, - {"1.2.3.4.5.6", "*.*.3.4.5.6", true}, - {"1.2.3.4.5.6", "*.*.*.4.5.6", true}, - {"1.2.3.4.5.6", "*.*.*.*.5.6", true}, - {"1.2.3.4.5.6", "*.*.*.*.*.6", true}, - {"1.2.3.4.5.6", "*.*.*.*.*.*", true}, - {"0.1.2.3.4.5.6", "*.*.*.*.*.*", false}, - {"1.2.3.4", "1.2.3.*", false}, // https://tools.ietf.org/html/rfc2818#section-3.1 - } { - actual := MatchWildcard(test.subject, test.wildcard) - if actual != test.expect { - t.Errorf("Test %d: Expected MatchWildcard(%s, %s)=%v, but got %v", - i, test.subject, test.wildcard, test.expect, actual) - } - } -} diff --git a/certmagic_test.go b/certmagic_test.go deleted file mode 100644 index 845fc245..00000000 --- a/certmagic_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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 - -// TODO diff --git a/config_test.go b/config_test.go deleted file mode 100644 index 84d4ce53..00000000 --- a/config_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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 ( - "context" - "os" - "reflect" - "testing" - - "github.com/mholt/acmez/acme" -) - -func TestSaveCertResource(t *testing.T) { - ctx := context.Background() - - am := &ACMEIssuer{CA: "https://example.com/acme/directory"} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - testStorageDir := testConfig.Storage.(*FileStorage).Path - defer func() { - err := os.RemoveAll(testStorageDir) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", testStorageDir, err) - } - }() - - domain := "example.com" - certContents := "certificate" - keyContents := "private key" - - cert := CertificateResource{ - SANs: []string{domain}, - PrivateKeyPEM: []byte(keyContents), - CertificatePEM: []byte(certContents), - IssuerData: &acme.Certificate{ - URL: "https://example.com/cert", - }, - issuerKey: am.IssuerKey(), - } - - err := testConfig.saveCertResource(ctx, am, cert) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // the result of our test will be a map, since we have - // no choice but to decode it into an 'any' interface - cert.IssuerData = map[string]any{ - "url": "https://example.com/cert", - } - - siteData, err := testConfig.loadCertResource(ctx, am, domain) - if err != nil { - t.Fatalf("Expected no error reading site, got: %v", err) - } - if !reflect.DeepEqual(cert, siteData) { - t.Errorf("Expected '%+v' to match '%+v'", cert, siteData) - } -} diff --git a/crypto_test.go b/crypto_test.go deleted file mode 100644 index 1ff65489..00000000 --- a/crypto_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// 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" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "testing" -) - -func TestEncodeDecodeRSAPrivateKey(t *testing.T) { - privateKey, err := rsa.GenerateKey(rand.Reader, 128) // make tests faster; small key size OK for testing - if err != nil { - t.Fatal(err) - } - - // test save - savedBytes, err := PEMEncodePrivateKey(privateKey) - if err != nil { - t.Fatal("error saving private key:", err) - } - - // test load - loadedKey, err := PEMDecodePrivateKey(savedBytes) - if err != nil { - t.Error("error loading private key:", err) - } - - // test load (should fail) - _, err = PEMDecodePrivateKey(savedBytes[2:]) - if err == nil { - t.Error("loading private key should have failed") - } - - // verify loaded key is correct - if !privateKeysSame(privateKey, loadedKey) { - t.Error("Expected key bytes to be the same, but they weren't") - } -} - -func TestSaveAndLoadECCPrivateKey(t *testing.T) { - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatal(err) - } - - // test save - savedBytes, err := PEMEncodePrivateKey(privateKey) - if err != nil { - t.Fatal("error saving private key:", err) - } - - // test load - loadedKey, err := PEMDecodePrivateKey(savedBytes) - if err != nil { - t.Error("error loading private key:", err) - } - - // verify loaded key is correct - if !privateKeysSame(privateKey, loadedKey) { - t.Error("Expected key bytes to be the same, but they weren't") - } -} - -// privateKeysSame compares the bytes of a and b and returns true if they are the same. -func privateKeysSame(a, b crypto.PrivateKey) bool { - return bytes.Equal(privateKeyBytes(a), privateKeyBytes(b)) -} - -// privateKeyBytes returns the bytes of DER-encoded key. -func privateKeyBytes(key crypto.PrivateKey) []byte { - var keyBytes []byte - switch key := key.(type) { - case *rsa.PrivateKey: - keyBytes = x509.MarshalPKCS1PrivateKey(key) - case *ecdsa.PrivateKey: - keyBytes, _ = x509.MarshalECPrivateKey(key) - case ed25519.PrivateKey: - return key - } - return keyBytes -} diff --git a/dnsutil.go b/dnsutil.go index 0d7ba74f..4cab7635 100644 --- a/dnsutil.go +++ b/dnsutil.go @@ -1,14 +1,10 @@ package certmagic import ( - "errors" - "fmt" "net" "strings" "sync" "time" - - "github.com/miekg/dns" ) // Code in this file adapted from go-acme/lego, July 2020: @@ -62,114 +58,6 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) return ent, nil } -func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { - var err error - var in *dns.Msg - - labelIndexes := dns.Split(fqdn) - for _, index := range labelIndexes { - domain := fqdn[index:] - - in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) - if err != nil { - continue - } - if in == nil { - continue - } - - switch in.Rcode { - case dns.RcodeSuccess: - // Check if we got a SOA RR in the answer section - if len(in.Answer) == 0 { - continue - } - - // CNAME records cannot/should not exist at the root of a zone. - // So we skip a domain when a CNAME is found. - if dnsMsgContainsCNAME(in) { - continue - } - - for _, ans := range in.Answer { - if soa, ok := ans.(*dns.SOA); ok { - return newSoaCacheEntry(soa), nil - } - } - case dns.RcodeNameError: - // NXDOMAIN - default: - // Any response code other than NOERROR and NXDOMAIN is treated as error - return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain) - } - } - - return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err)) -} - -// dnsMsgContainsCNAME checks for a CNAME answer in msg -func dnsMsgContainsCNAME(msg *dns.Msg) bool { - for _, ans := range msg.Answer { - if _, ok := ans.(*dns.CNAME); ok { - return true - } - } - return false -} - -func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) { - m := createDNSMsg(fqdn, rtype, recursive) - var in *dns.Msg - var err error - for _, ns := range nameservers { - in, err = sendDNSQuery(m, ns) - if err == nil && len(in.Answer) > 0 { - break - } - } - return in, err -} - -func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg { - m := new(dns.Msg) - m.SetQuestion(fqdn, rtype) - - // See: https://caddy.community/t/hard-time-getting-a-response-on-a-dns-01-challenge/15721/16 - m.SetEdns0(1232, false) - if !recursive { - m.RecursionDesired = false - } - return m -} - -func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) { - udp := &dns.Client{Net: "udp", Timeout: dnsTimeout} - in, _, err := udp.Exchange(m, ns) - // two kinds of errors we can handle by retrying with TCP: - // truncation and timeout; see https://github.com/caddyserver/caddy/issues/3639 - truncated := in != nil && in.Truncated - timeoutErr := err != nil && strings.Contains(err.Error(), "timeout") - if truncated || timeoutErr { - tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} - in, _, err = tcp.Exchange(m, ns) - } - return in, err -} - -func formatDNSError(msg *dns.Msg, err error) string { - var parts []string - if msg != nil { - parts = append(parts, dns.RcodeToString[msg.Rcode]) - } - if err != nil { - parts = append(parts, err.Error()) - } - if len(parts) > 0 { - return ": " + strings.Join(parts, " ") - } - return "" -} - // soaCacheEntry holds a cached SOA record (only selected fields) type soaCacheEntry struct { zone string // zone apex (a domain name) @@ -177,29 +65,11 @@ type soaCacheEntry struct { expires time.Time // time when this cache entry should be evicted } -func newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry { - return &soaCacheEntry{ - zone: soa.Hdr.Name, - primaryNs: soa.Ns, - expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second), - } -} - // isExpired checks whether a cache entry should be considered expired. func (cache *soaCacheEntry) isExpired() bool { return time.Now().After(cache.expires) } -// systemOrDefaultNameservers attempts to get system nameservers from the -// resolv.conf file given by path before falling back to hard-coded defaults. -func systemOrDefaultNameservers(path string, defaults []string) []string { - config, err := dns.ClientConfigFromFile(path) - if err != nil || len(config.Servers) == 0 { - return defaults - } - return config.Servers -} - // populateNameserverPorts ensures that all nameservers have a port number. func populateNameserverPorts(servers []string) { for i := range servers { @@ -210,105 +80,6 @@ func populateNameserverPorts(servers []string) { } } -// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. -func checkDNSPropagation(fqdn, value string, resolvers []string) (bool, error) { - if !strings.HasSuffix(fqdn, ".") { - fqdn += "." - } - - // Initial attempt to resolve at the recursive NS - r, err := dnsQuery(fqdn, dns.TypeTXT, resolvers, true) - if err != nil { - return false, err - } - - if r.Rcode == dns.RcodeSuccess { - fqdn = updateDomainWithCName(r, fqdn) - } - - authoritativeNss, err := lookupNameservers(fqdn, resolvers) - if err != nil { - return false, err - } - - return checkAuthoritativeNss(fqdn, value, authoritativeNss) -} - -// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. -func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { - for _, ns := range nameservers { - r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) - if err != nil { - return false, err - } - - if r.Rcode != dns.RcodeSuccess { - if r.Rcode == dns.RcodeNameError { - // if Present() succeeded, then it must show up eventually, or else - // something is really broken in the DNS provider or their API; - // no need for error here, simply have the caller try again - return false, nil - } - return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) - } - - var found bool - for _, rr := range r.Answer { - if txt, ok := rr.(*dns.TXT); ok { - record := strings.Join(txt.Txt, "") - if record == value { - found = true - break - } - } - } - - if !found { - return false, nil - } - } - - return true, nil -} - -// lookupNameservers returns the authoritative nameservers for the given fqdn. -func lookupNameservers(fqdn string, resolvers []string) ([]string, error) { - var authoritativeNss []string - - zone, err := findZoneByFQDN(fqdn, resolvers) - if err != nil { - return nil, fmt.Errorf("could not determine the zone: %w", err) - } - - r, err := dnsQuery(zone, dns.TypeNS, resolvers, true) - if err != nil { - return nil, err - } - - for _, rr := range r.Answer { - if ns, ok := rr.(*dns.NS); ok { - authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) - } - } - - if len(authoritativeNss) > 0 { - return authoritativeNss, nil - } - return nil, errors.New("could not determine authoritative nameservers") -} - -// Update FQDN with CNAME if any -func updateDomainWithCName(r *dns.Msg, fqdn string) string { - for _, rr := range r.Answer { - if cn, ok := rr.(*dns.CNAME); ok { - if cn.Hdr.Name == fqdn { - return cn.Target - } - } - } - return fqdn -} - // recursiveNameservers are used to pre-check DNS propagation. It // picks user-configured nameservers (custom) OR the defaults // obtained from resolv.conf and defaultNameservers if none is @@ -316,7 +87,7 @@ func updateDomainWithCName(r *dns.Msg, fqdn string) string { func recursiveNameservers(custom []string) []string { var servers []string if len(custom) == 0 { - servers = systemOrDefaultNameservers(defaultResolvConf, defaultNameservers) + servers = defaultNameservers } else { servers = make([]string, len(custom)) copy(servers, custom) diff --git a/dnsutil_replace.go b/dnsutil_replace.go new file mode 100644 index 00000000..60e6485b --- /dev/null +++ b/dnsutil_replace.go @@ -0,0 +1,270 @@ +//go:build !with_miekg_dns + +package certmagic + +import ( + "strings" + "fmt" + "time" + "net" + "errors" + "golang.org/x/net/dns/dnsmessage" + "github.com/sagernet/sing-dns" + "context" + "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/metadata" +) + +func dnsSplit(s string) []int { + if s == "." { + return nil + } + idx := make([]int, 1, 3) + off := 0 + end := false + + for { + off, end = dnsNextLabel(s, off) + if end { + return idx + } + idx = append(idx, off) + } +} + +func dnsNextLabel(s string, offset int) (i int, end bool) { + if s == "" { + return 0, true + } + for i = offset; i < len(s)-1; i++ { + if s[i] != '.' { + continue + } + j := i - 1 + for j >= 0 && s[j] == '\\' { + j-- + } + + if (j-i)%2 == 0 { + continue + } + + return i + 1, false + } + return i + 1, true +} + +func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { + var err error + var in *dnsmessage.Message + labelIndexes := dnsSplit(fqdn) + for _, index := range labelIndexes { + domain := fqdn[index:] + + in, err = dnsQuery(domain, dnsmessage.TypeSOA, nameservers, true) + if err != nil { + continue + } + if in == nil { + continue + } + + switch in.RCode { + case dnsmessage.RCodeSuccess: + // Check if we got a SOA RR in the answer section + if len(in.Answers) == 0 { + continue + } + + // CNAME records cannot/should not exist at the root of a zone. + // So we skip a domain when a CNAME is found. + if dnsMsgContainsCNAME(in) { + continue + } + + for _, ans := range in.Answers { + if soa, ok := ans.Body.(*dnsmessage.SOAResource); ok { + return newSoaCacheEntry(ans.Header.Name.String(), soa), nil + } + } + case dnsmessage.RCodeNameError: + // NXDOMAIN + default: + // Any response code other than NOERROR and NXDOMAIN is treated as error + return nil, fmt.Errorf("unexpected response code '%s' for %s", in.RCode.String(), domain) + } + } + + return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err)) +} + +// dnsMsgContainsCNAME checks for a CNAME answer in msg +func dnsMsgContainsCNAME(msg *dnsmessage.Message) bool { + for _, ans := range msg.Answers { + if _, ok := ans.Body.(*dnsmessage.CNAMEResource); ok { + return true + } + } + return false +} + +func dnsQuery(fqdn string, rtype dnsmessage.Type, nameservers []string, recursive bool) (*dnsmessage.Message, error) { + m := createDNSMsg(fqdn, rtype, recursive) + var in *dnsmessage.Message + var err error + for _, ns := range nameservers { + in, err = sendDNSQuery(m, ns) + if err == nil && len(in.Answers) > 0 { + break + } + } + return in, err +} + +func createDNSMsg(fqdn string, rtype dnsmessage.Type, recursive bool) *dnsmessage.Message { + m := new(dnsmessage.Message) + m.Questions = []dnsmessage.Question{{Name: dnsmessage.MustNewName(fqdn), Type: rtype, Class: dnsmessage.ClassINET}} + var edns0Hdr dnsmessage.ResourceHeader + edns0Hdr.SetEDNS0(1232, 0, false) + m.Additionals = append(m.Additionals, dnsmessage.Resource{Header: edns0Hdr}) + if !recursive { + m.RecursionDesired = false + } + return m +} + +func sendDNSQuery(m *dnsmessage.Message, ns string) (*dnsmessage.Message, error) { + var client dns.Transport + client = dns.NewUDPTransport(context.Background(), network.SystemDialer, metadata.ParseSocksaddr(ns)) + defer client.Close() + resp, err := client.Exchange(context.Background(), m) + if err == nil { + return resp, nil + } + if err.Error() == "insufficient data for base length type" { + client.Close() + client = dns.NewTCPTransport(context.Background(), network.SystemDialer, metadata.ParseSocksaddr(ns)) + } + return client.Exchange(context.Background(), m) +} + +func formatDNSError(msg *dnsmessage.Message, err error) string { + var parts []string + if msg != nil { + parts = append(parts, msg.RCode.String()) + } + if err != nil { + parts = append(parts, err.Error()) + } + if len(parts) > 0 { + return ": " + strings.Join(parts, " ") + } + return "" +} + +func newSoaCacheEntry(zone string, soa *dnsmessage.SOAResource) *soaCacheEntry { + return &soaCacheEntry{ + zone: zone, + primaryNs: soa.NS.String(), + expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second), + } +} + +// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. +func checkDNSPropagation(fqdn, value string, resolvers []string) (bool, error) { + if !strings.HasSuffix(fqdn, ".") { + fqdn += "." + } + + // Initial attempt to resolve at the recursive NS + r, err := dnsQuery(fqdn, dnsmessage.TypeTXT, resolvers, true) + if err != nil { + return false, err + } + + if r.RCode == dnsmessage.RCodeSuccess { + fqdn = updateDomainWithCName(r, fqdn) + } + + authoritativeNss, err := lookupNameservers(fqdn, resolvers) + if err != nil { + return false, err + } + + return checkAuthoritativeNss(fqdn, value, authoritativeNss) +} + +// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. +func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { + for _, ns := range nameservers { + r, err := dnsQuery(fqdn, dnsmessage.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) + if err != nil { + return false, err + } + + if r.RCode != dnsmessage.RCodeSuccess { + if r.RCode == dnsmessage.RCodeNameError { + // if Present() succeeded, then it must show up eventually, or else + // something is really broken in the DNS provider or their API; + // no need for error here, simply have the caller try again + return false, nil + } + return false, fmt.Errorf("NS %s returned %s for %s", ns, r.RCode.String(), fqdn) + } + + var found bool + for _, rr := range r.Answers { + if txt, ok := rr.Body.(*dnsmessage.TXTResource); ok { + record := strings.Join(txt.TXT, "") + if record == value { + found = true + break + } + } + } + + if !found { + return false, nil + } + } + + return true, nil +} + +// lookupNameservers returns the authoritative nameservers for the given fqdn. +func lookupNameservers(fqdn string, resolvers []string) ([]string, error) { + var authoritativeNss []string + + zone, err := findZoneByFQDN(fqdn, resolvers) + if err != nil { + return nil, fmt.Errorf("could not determine the zone: %w", err) + } + + r, err := dnsQuery(zone, dnsmessage.TypeNS, resolvers, true) + if err != nil { + return nil, err + } + + for _, rr := range r.Answers { + if ns, ok := rr.Body.(*dnsmessage.NSResource); ok { + authoritativeNss = append(authoritativeNss, strings.ToLower(ns.NS.String())) + } + } + + if len(authoritativeNss) > 0 { + return authoritativeNss, nil + } + return nil, errors.New("could not determine authoritative nameservers") +} + +// Update FQDN with CNAME if any +func updateDomainWithCName(r *dnsmessage.Message, fqdn string) string { + for _, rr := range r.Answers { + if cn, ok := rr.Body.(*dnsmessage.CNAMEResource); ok { + if rr.Header.Name.String() == fqdn { + return cn.CNAME.String() + } + } + } + return fqdn +} diff --git a/dnsutil_test.go b/dnsutil_test.go deleted file mode 100644 index a05d8f48..00000000 --- a/dnsutil_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package certmagic - -// Code in this file adapted from go-acme/lego, July 2020: -// https://github.com/go-acme/lego -// by Ludovic Fernandez and Dominik Menke -// -// It has been modified. - -import ( - "net" - "reflect" - "sort" - "strings" - "testing" -) - -func TestLookupNameserversOK(t *testing.T) { - testCases := []struct { - fqdn string - nss []string - }{ - { - fqdn: "books.google.com.ng.", - nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, - }, - { - fqdn: "www.google.com.", - nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, - }, - { - fqdn: "physics.georgetown.edu.", - nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."}, - }, - } - - for i, test := range testCases { - test := test - t.Run(test.fqdn, func(t *testing.T) { - t.Parallel() - - nss, err := lookupNameservers(test.fqdn, recursiveNameservers(nil)) - if err != nil { - t.Errorf("Expected no error, got: %v", err) - } - - sort.Strings(nss) - sort.Strings(test.nss) - - if !reflect.DeepEqual(test.nss, nss) { - t.Errorf("Test %d: expected %+v but got %+v", i, test.nss, nss) - } - }) - } -} - -func TestLookupNameserversErr(t *testing.T) { - testCases := []struct { - desc string - fqdn string - error string - }{ - { - desc: "invalid tld", - fqdn: "_null.n0n0.", - error: "could not determine the zone", - }, - } - - for i, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := lookupNameservers(test.fqdn, nil) - if err == nil { - t.Errorf("expected error, got none") - } - if !strings.Contains(err.Error(), test.error) { - t.Errorf("Test %d: Expected error to contain '%s' but got '%s'", i, test.error, err.Error()) - } - }) - } -} - -var findXByFqdnTestCases = []struct { - desc string - fqdn string - zone string - primaryNs string - nameservers []string - expectedError string -}{ - { - desc: "domain is a CNAME", - fqdn: "scholar.google.com.", - zone: "google.com.", - primaryNs: "ns1.google.com.", - nameservers: recursiveNameservers(nil), - }, - { - desc: "domain is a non-existent subdomain", - fqdn: "foo.google.com.", - zone: "google.com.", - primaryNs: "ns1.google.com.", - nameservers: recursiveNameservers(nil), - }, - { - desc: "domain is a eTLD", - fqdn: "example.com.ac.", - zone: "ac.", - primaryNs: "a0.nic.ac.", - nameservers: recursiveNameservers(nil), - }, - { - desc: "domain is a cross-zone CNAME", - fqdn: "cross-zone-example.assets.sh.", - zone: "assets.sh.", - primaryNs: "gina.ns.cloudflare.com.", - nameservers: recursiveNameservers(nil), - }, - { - desc: "NXDOMAIN", - fqdn: "test.loho.jkl.", - zone: "loho.jkl.", - nameservers: []string{"1.1.1.1:53"}, - expectedError: "could not find the start of authority for test.loho.jkl.: NXDOMAIN", - }, - { - desc: "several non existent nameservers", - fqdn: "scholar.google.com.", - zone: "google.com.", - primaryNs: "ns1.google.com.", - nameservers: []string{":7053", ":8053", "1.1.1.1:53"}, - }, - { - desc: "only non existent nameservers", - fqdn: "scholar.google.com.", - zone: "google.com.", - nameservers: []string{":7053", ":8053", ":9053"}, - expectedError: "could not find the start of authority for scholar.google.com.:", - }, - { - desc: "no nameservers", - fqdn: "test.ldez.com.", - zone: "ldez.com.", - nameservers: []string{}, - expectedError: "could not find the start of authority for test.ldez.com.", - }, -} - -func TestFindZoneByFqdn(t *testing.T) { - for i, test := range findXByFqdnTestCases { - t.Run(test.desc, func(t *testing.T) { - clearFqdnCache() - - zone, err := findZoneByFQDN(test.fqdn, test.nameservers) - if test.expectedError != "" { - if err == nil { - t.Errorf("test %d: expected error, got none", i) - return - } - if !strings.Contains(err.Error(), test.expectedError) { - t.Errorf("test %d: expected error to contain '%s' but got '%s'", i, test.expectedError, err.Error()) - } - } else { - if err != nil { - t.Errorf("test %d: expected no error, but got: %v", i, err) - } - if zone != test.zone { - t.Errorf("test %d: expected zone '%s' but got '%s'", i, zone, test.zone) - } - } - }) - } -} - -func TestResolveConfServers(t *testing.T) { - var testCases = []struct { - fixture string - expected []string - defaults []string - }{ - { - fixture: "testdata/resolv.conf.1", - defaults: []string{"127.0.0.1:53"}, - expected: []string{"10.200.3.249", "10.200.3.250:5353", "2001:4860:4860::8844", "[10.0.0.1]:5353"}, - }, - { - fixture: "testdata/resolv.conf.nonexistant", - defaults: []string{"127.0.0.1:53"}, - expected: []string{"127.0.0.1:53"}, - }, - } - - for i, test := range testCases { - t.Run(test.fixture, func(t *testing.T) { - result := systemOrDefaultNameservers(test.fixture, test.defaults) - - sort.Strings(result) - sort.Strings(test.expected) - - if !reflect.DeepEqual(test.expected, result) { - t.Errorf("Test %d: Expected %v but got %v", i, test.expected, result) - } - }) - } -} - -func TestRecursiveNameserversAddsPort(t *testing.T) { - type want struct { - port string - } - custom := []string{"127.0.0.1", "ns1.google.com:43"} - expectations := []want{{port: "53"}, {port: "43"}} - results := recursiveNameservers(custom) - - if !reflect.DeepEqual(custom, []string{"127.0.0.1", "ns1.google.com:43"}) { - t.Errorf("Expected custom nameservers to be unmodified. got %v", custom) - } - - if len(results) != len(expectations) { - t.Errorf("%v wrong results length. got %d, want %d", results, len(results), len(expectations)) - } - - var hasCustom bool - for i, res := range results { - hasCustom = hasCustom || strings.HasPrefix(res, custom[0]) - if _, port, err := net.SplitHostPort(res); err != nil { - t.Errorf("%v Error splitting result %d into host and port: %v", results, i, err) - } else { - if port != expectations[i].port { - t.Errorf("%v Expected result %d to have port %s but got %s", results, i, expectations[i].port, port) - } - } - } - if !hasCustom { - t.Errorf("%v Expected custom resolvers to be included, but they weren't: %v", results, custom) - } - -} - -func TestRecursiveNameserversDefaults(t *testing.T) { - results := recursiveNameservers(nil) - if len(results) < 1 { - t.Errorf("%v Expected at least 1 records as default when nil custom", results) - } - - results = recursiveNameservers([]string{}) - if len(results) < 1 { - t.Errorf("%v Expected at least 1 records as default when empty custom", results) - } -} - -func clearFqdnCache() { - fqdnSOACacheMu.Lock() - fqdnSOACache = make(map[string]*soaCacheEntry) - fqdnSOACacheMu.Unlock() -} diff --git a/doc_test.go b/doc_test.go deleted file mode 100644 index f053fd9e..00000000 --- a/doc_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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" - "log" - "net/http" -) - -// This is the simplest way for HTTP servers to use this package. -// Call HTTPS() with your domain names and your handler (or nil -// for the http.DefaultMux), and CertMagic will do the rest. -func ExampleHTTPS() { - http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "Hello, HTTPS visitor!") - }) - - err := HTTPS([]string{"example.com", "www.example.com"}, nil) - if err != nil { - log.Fatal(err) - } -} diff --git a/go.mod b/go.mod index ba3704d5..26f1af98 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/caddyserver/certmagic +module github.com/sagernet/certmagic go 1.18 @@ -6,18 +6,30 @@ require ( github.com/klauspost/cpuid/v2 v2.1.0 github.com/libdns/libdns v0.2.1 github.com/mholt/acmez v1.0.4 - github.com/miekg/dns v1.1.50 - go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 + github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0 + github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9 + go.uber.org/zap v1.22.0 + golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 + golang.org/x/net v0.0.0-20220812174116-3211cb980234 ) require ( + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/marten-seemann/qpack v0.2.1 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect + golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect + golang.org/x/tools v0.1.10 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) diff --git a/go.sum b/go.sum index f9ebdb0a..b9dc3c74 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,25 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -10,18 +29,41 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= +github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= +github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb h1:wc0yQ+SBn4TaTYRwpwvEm3nc4eRdxk6vtRbouLVZAzk= +github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb/go.mod h1:MIccjRKnPTjWwAOpl+AUGWOkzyTd9tERytudxu+1ra4= +github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0 h1:jFtynAm5qU1WXIs4FBxi7nLtTVMNXIv/hgO0V/BxmuE= +github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= +github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9 h1:XgXSOJv8e7+98SJvg1f0luuPR33r4yFcmzxb3R//BTI= +github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9/go.mod h1:MAHy2IKZAA101t3Gr2x0ldwn6XuAs2cjGzSzHy5RhWk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -29,59 +71,91 @@ go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0= +go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c= +golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 h1:N9Vc/rorQUDes6B9CNdIxAn5jODGj2wzfrei2x4wNj4= -golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= +golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 h1:fqTvyMIIj+HRzMmnzr9NtpHP6uVpvB5fkHcgPDC4nu8= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/handshake_test.go b/handshake_test.go deleted file mode 100644 index e5d6efcb..00000000 --- a/handshake_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// 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" - "net" - "testing" -) - -func TestGetCertificate(t *testing.T) { - c := &Cache{ - cache: make(map[string]Certificate), - cacheIndex: make(map[string][]string), - } - cfg := &Config{certCache: c} - - // create a test connection for conn.LocalAddr() - l, _ := net.Listen("tcp", "127.0.0.1:0") - defer l.Close() - conn, _ := net.Dial("tcp", l.Addr().String()) - if conn == nil { - t.Errorf("failed to create a test connection") - } - defer conn.Close() - - hello := &tls.ClientHelloInfo{ServerName: "example.com", Conn: conn} - helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com", Conn: conn} - helloNoSNI := &tls.ClientHelloInfo{Conn: conn} - helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch", Conn: conn} - - // When cache is empty - if cert, err := cfg.GetCertificate(hello); err == nil { - t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert) - } - if cert, err := cfg.GetCertificate(helloNoSNI); err == nil { - t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert) - } - - // When cache has one certificate in it - firstCert := Certificate{Names: []string{"example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}} - c.cacheCertificate(firstCert) - if cert, err := cfg.GetCertificate(hello); err != nil { - t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err) - } else if cert.Leaf.DNSNames[0] != "example.com" { - t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert) - } - if _, err := cfg.GetCertificate(helloNoSNI); err == nil { - t.Errorf("Did not get an error with no SNI and no DefaultServerName, but should have: %v", err) - } - - // When retrieving wildcard certificate - wildcardCert := Certificate{ - Names: []string{"*.example.com"}, - Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}, - hash: "(don't overwrite the first one)", - } - c.cacheCertificate(wildcardCert) - if cert, err := cfg.GetCertificate(helloSub); err != nil { - t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err) - } else if cert.Leaf.DNSNames[0] != "*.example.com" { - t.Errorf("Got wrong certificate, expected wildcard: %v", cert) - } - - // When cache is NOT empty but there's no SNI - if _, err := cfg.GetCertificate(helloNoSNI); err == nil { - t.Errorf("Expected TLS allert when no SNI and no DefaultServerName, but got: %v", err) - } - - // When no certificate matches, raise an alert - if _, err := cfg.GetCertificate(helloNoMatch); err == nil { - t.Errorf("Expected an error when no certificate matched the SNI, got: %v", err) - } - - // When default SNI is set and SNI is missing, retrieve default cert - cfg.DefaultServerName = "example.com" - if cert, err := cfg.GetCertificate(helloNoSNI); err != nil { - t.Errorf("Got an error with no SNI with DefaultServerName, but shouldn't have: %v", err) - } else if cert == nil || cert.Leaf.DNSNames[0] != "example.com" { - t.Errorf("Expected default cert, got: %v", cert) - } - - // When default SNI is set and SNI is missing but IP address matches, retrieve IP cert - ipCert := Certificate{ - Names: []string{"127.0.0.1"}, - Certificate: tls.Certificate{Leaf: &x509.Certificate{IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}}}, - hash: "(don't overwrite the first or second one)", - } - c.cacheCertificate(ipCert) - if cert, err := cfg.GetCertificate(helloNoSNI); err != nil { - t.Errorf("Got an error with no SNI but matching IP, but shouldn't have: %v", err) - } else if cert == nil || len(cert.Leaf.IPAddresses) == 0 { - t.Errorf("Expected IP cert, got: %v", cert) - } -} diff --git a/httphandler_test.go b/httphandler_test.go deleted file mode 100644 index 8b09af84..00000000 --- a/httphandler_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// 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/http" - "net/http/httptest" - "os" - "testing" -) - -func TestHTTPChallengeHandlerNoOp(t *testing.T) { - am := &ACMEIssuer{CA: "https://example.com/acme/directory"} - testConfig := &Config{ - Issuers: []Issuer{am}, - Storage: &FileStorage{Path: "./_testdata_tmp"}, - certCache: new(Cache), - } - am.config = testConfig - - testStorageDir := testConfig.Storage.(*FileStorage).Path - defer func() { - err := os.RemoveAll(testStorageDir) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", testStorageDir, err) - } - }() - - // try base paths and host names that aren't - // handled by this handler - for _, url := range []string{ - "http://localhost/", - "http://localhost/foo.html", - "http://localhost/.git", - "http://localhost/.well-known/", - "http://localhost/.well-known/acme-challenging", - "http://other/.well-known/acme-challenge/foo", - } { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - t.Fatalf("Could not craft request, got error: %v", err) - } - rw := httptest.NewRecorder() - if am.HandleHTTPChallenge(rw, req) { - t.Errorf("Got true with this URL, but shouldn't have: %s", url) - } - } -} diff --git a/solvers.go b/solvers.go index a7132ee3..524b87e1 100644 --- a/solvers.go +++ b/solvers.go @@ -32,7 +32,8 @@ import ( "github.com/libdns/libdns" "github.com/mholt/acmez" "github.com/mholt/acmez/acme" - "github.com/miekg/dns" + "strconv" + "errors" ) // httpSolver solves the HTTP challenge. It must be @@ -703,7 +704,7 @@ type Challenge struct { // unless it is an IP address using the TLS-ALPN challenge. func challengeKey(chal acme.Challenge) string { if chal.Type == acme.ChallengeTypeTLSALPN01 && chal.Identifier.Type == "ip" { - reversed, err := dns.ReverseAddr(chal.Identifier.Value) + reversed, err := reverseAddr(chal.Identifier.Value) if err == nil { return reversed[:len(reversed)-1] // strip off '.' } @@ -711,6 +712,36 @@ func challengeKey(chal acme.Challenge) string { return chal.Identifier.Value } +const hexDigit = "0123456789abcdef" + +func reverseAddr(addr string) (arpa string, err error) { + ip := net.ParseIP(addr) + if ip == nil { + return "", errors.New("unrecognized address: " + addr) + } + if v4 := ip.To4(); v4 != nil { + buf := make([]byte, 0, net.IPv4len*4+len("in-addr.arpa.")) + // Add it, in reverse, to the buffer + for i := len(v4) - 1; i >= 0; i-- { + buf = strconv.AppendInt(buf, int64(v4[i]), 10) + buf = append(buf, '.') + } + // Append "in-addr.arpa." and return (buf already has the final .) + buf = append(buf, "in-addr.arpa."...) + return string(buf), nil + } + // Must be IPv6 + buf := make([]byte, 0, net.IPv6len*4+len("ip6.arpa.")) + // Add it, in reverse, to the buffer + for i := len(ip) - 1; i >= 0; i-- { + v := ip[i] + buf = append(buf, hexDigit[v&0xF], '.', hexDigit[v>>4], '.') + } + // Append "ip6.arpa." and return (buf already has the final .) + buf = append(buf, "ip6.arpa."...) + return string(buf), nil +} + // solverWrapper should be used to wrap all challenge solvers so that // we can add the challenge info to memory; this makes challenges globally // solvable by a single HTTP or TLS server even if multiple servers with diff --git a/solvers_test.go b/solvers_test.go deleted file mode 100644 index 21deaead..00000000 --- a/solvers_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// 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 ( - "testing" - - "github.com/mholt/acmez/acme" -) - -func Test_challengeKey(t *testing.T) { - type args struct { - chal acme.Challenge - } - tests := []struct { - name string - args args - want string - }{ - { - name: "ok/dns-dns", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeDNS01, - Identifier: acme.Identifier{ - Type: "dns", - Value: "*.example.com", - }, - }, - }, - want: "*.example.com", - }, - { - name: "ok/http-dns", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeHTTP01, - Identifier: acme.Identifier{ - Type: "dns", - Value: "*.example.com", - }, - }, - }, - want: "*.example.com", - }, - { - name: "ok/tls-dns", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeTLSALPN01, - Identifier: acme.Identifier{ - Type: "dns", - Value: "*.example.com", - }, - }, - }, - want: "*.example.com", - }, - { - name: "ok/http-ipv4", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeHTTP01, - Identifier: acme.Identifier{ - Type: "ip", - Value: "127.0.0.1", - }, - }, - }, - want: "127.0.0.1", - }, - { - name: "ok/http-ipv6", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeHTTP01, - Identifier: acme.Identifier{ - Type: "ip", - Value: "2001:db8::1", - }, - }, - }, - want: "2001:db8::1", - }, - { - name: "ok/tls-ipv4", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeTLSALPN01, - Identifier: acme.Identifier{ - Type: "ip", - Value: "127.0.0.1", - }, - }, - }, - want: "1.0.0.127.in-addr.arpa", - }, - { - name: "ok/tls-ipv6", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeTLSALPN01, - Identifier: acme.Identifier{ - Type: "ip", - Value: "2001:db8::1", - }, - }, - }, - want: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", - }, - { - name: "fail/tls-ipv4", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeTLSALPN01, - Identifier: acme.Identifier{ - Type: "ip", - Value: "127.0.0.1000", - }, - }, - }, - want: "127.0.0.1000", // reversing this fails; default to identifier value - }, - { - name: "fail/tls-ipv6", - args: args{ - chal: acme.Challenge{ - Type: acme.ChallengeTypeTLSALPN01, - Identifier: acme.Identifier{ - Type: "ip", - Value: "2001:db8::10000", - }, - }, - }, - want: "2001:db8::10000", // reversing this fails; default to identifier value - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := challengeKey(tt.args.chal); got != tt.want { - t.Errorf("challengeKey() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/storage_test.go b/storage_test.go deleted file mode 100644 index ecefed84..00000000 --- a/storage_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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 ( - "path" - "testing" -) - -func TestPrefixAndKeyBuilders(t *testing.T) { - am := &ACMEIssuer{CA: "https://example.com/acme-ca/directory"} - - base := path.Join("certificates", "example.com-acme-ca-directory") - - for i, testcase := range []struct { - in, folder, certFile, keyFile, metaFile string - }{ - { - in: "example.com", - folder: path.Join(base, "example.com"), - certFile: path.Join(base, "example.com", "example.com.crt"), - keyFile: path.Join(base, "example.com", "example.com.key"), - metaFile: path.Join(base, "example.com", "example.com.json"), - }, - { - in: "*.example.com", - folder: path.Join(base, "wildcard_.example.com"), - certFile: path.Join(base, "wildcard_.example.com", "wildcard_.example.com.crt"), - keyFile: path.Join(base, "wildcard_.example.com", "wildcard_.example.com.key"), - metaFile: path.Join(base, "wildcard_.example.com", "wildcard_.example.com.json"), - }, - { - // prevent directory traversal! very important, esp. with on-demand TLS - // see issue #2092 - in: "a/../../../foo", - folder: path.Join(base, "afoo"), - certFile: path.Join(base, "afoo", "afoo.crt"), - keyFile: path.Join(base, "afoo", "afoo.key"), - metaFile: path.Join(base, "afoo", "afoo.json"), - }, - { - in: "b\\..\\..\\..\\foo", - folder: path.Join(base, "bfoo"), - certFile: path.Join(base, "bfoo", "bfoo.crt"), - keyFile: path.Join(base, "bfoo", "bfoo.key"), - metaFile: path.Join(base, "bfoo", "bfoo.json"), - }, - { - in: "c/foo", - folder: path.Join(base, "cfoo"), - certFile: path.Join(base, "cfoo", "cfoo.crt"), - keyFile: path.Join(base, "cfoo", "cfoo.key"), - metaFile: path.Join(base, "cfoo", "cfoo.json"), - }, - } { - if actual := StorageKeys.SiteCert(am.IssuerKey(), testcase.in); actual != testcase.certFile { - t.Errorf("Test %d: site cert file: Expected '%s' but got '%s'", i, testcase.certFile, actual) - } - if actual := StorageKeys.SitePrivateKey(am.IssuerKey(), testcase.in); actual != testcase.keyFile { - t.Errorf("Test %d: site key file: Expected '%s' but got '%s'", i, testcase.keyFile, actual) - } - if actual := StorageKeys.SiteMeta(am.IssuerKey(), testcase.in); actual != testcase.metaFile { - t.Errorf("Test %d: site meta file: Expected '%s' but got '%s'", i, testcase.metaFile, actual) - } - } -} diff --git a/testdata/resolv.conf.1 b/testdata/resolv.conf.1 deleted file mode 100644 index 3098f99b..00000000 --- a/testdata/resolv.conf.1 +++ /dev/null @@ -1,5 +0,0 @@ -domain company.com -nameserver 10.200.3.249 -nameserver 10.200.3.250:5353 -nameserver 2001:4860:4860::8844 -nameserver [10.0.0.1]:5353