Merge remote-tracking branch 'challtestsrv/main'
This commit is contained in:
commit
e73ff76da4
|
|
@ -0,0 +1,79 @@
|
|||
# Challenge Test Server
|
||||
|
||||
[](https://travis-ci.org/letsencrypt/challtestsrv)
|
||||
[](https://coveralls.io/github/letsencrypt/challtestsrv)
|
||||
[](https://goreportcard.com/report/github.com/letsencrypt/challtestsrv)
|
||||
[](https://golangci.com/r/github.com/letsencrypt/challtestsrv)
|
||||
|
||||
The `challtestsrv` package offers a library that can be used by test
|
||||
code to respond to HTTP-01, DNS-01, and TLS-ALPN-01 ACME challenges. The
|
||||
`challtestsrv` package can also be used as a mock DNS server letting
|
||||
developers mock `A`, `AAAA`, `CNAME`, and `CAA` DNS data for specific hostnames.
|
||||
The mock server will resolve up to one level of `CNAME` aliasing for accepted
|
||||
DNS request types.
|
||||
|
||||
**Important note: The `challtestsrv` library is for TEST USAGE
|
||||
ONLY. It is trivially insecure, offering no authentication. Only use
|
||||
`challtestsrv` in a controlled test environment.**
|
||||
|
||||
For example this package is used by the Boulder
|
||||
[`load-generator`](https://github.com/letsencrypt/boulder/tree/9e39680e3f78c410e2d780a7badfe200a31698eb/test/load-generator)
|
||||
command to manage its own in-process HTTP-01 challenge server.
|
||||
|
||||
### Usage
|
||||
|
||||
Create a challenge server responding to HTTP-01 challenges on ":8888" and
|
||||
DNS-01 challenges on ":9999" and "10.0.0.1:9998":
|
||||
|
||||
```
|
||||
import "github.com/letsencrypt/pebble/challtestsrv"
|
||||
|
||||
challSrv, err := challtestsrv.New(challsrv.Config{
|
||||
HTTPOneAddr: []string{":8888"},
|
||||
DNSOneAddr: []string{":9999", "10.0.0.1:9998"},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
Run the Challenge server and subservers:
|
||||
```
|
||||
// Start the Challenge server in its own Go routine
|
||||
go challSrv.Run()
|
||||
```
|
||||
|
||||
Add an HTTP-01 response for the token `"aaa"` and the value `"bbb"`, defer
|
||||
cleaning it up again:
|
||||
```
|
||||
challSrv.AddHTTPOneChallenge("aaa", "bbb")
|
||||
defer challSrv.DeleteHTTPOneChallenge("aaa")
|
||||
```
|
||||
|
||||
Add a DNS-01 TXT response for the host `"_acme-challenge.example.com."` and the
|
||||
value `"bbb"`, defer cleaning it up again:
|
||||
```
|
||||
challSrv.AddDNSOneChallenge("_acme-challenge.example.com.", "bbb")
|
||||
defer challSrv.DeleteHTTPOneChallenge("_acme-challenge.example.com.")
|
||||
```
|
||||
|
||||
Get the history of HTTP requests processed by the challenge server for the host
|
||||
"example.com":
|
||||
```
|
||||
requestHistory := challSrv.RequestHistory("example.com", challtestsrv.HTTPRequestEventType)
|
||||
```
|
||||
|
||||
Clear the history of HTTP requests processed by the challenge server for the
|
||||
host "example.com":
|
||||
```
|
||||
challSrv.ClearRequestHistory("example.com", challtestsrv.HTTPRequestEventType)
|
||||
```
|
||||
|
||||
Stop the Challenge server and subservers:
|
||||
```
|
||||
// Shutdown the Challenge server
|
||||
challSrv.Shutdown()
|
||||
```
|
||||
|
||||
For more information on the package API see Godocs and the associated package
|
||||
sourcecode.
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
// Package challtestsrv provides a trivially insecure acme challenge response
|
||||
// server for rapidly testing HTTP-01, DNS-01 and TLS-ALPN-01 challenge types.
|
||||
package challtestsrv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default to using localhost for both A and AAAA queries that don't match
|
||||
// more specific mock host data.
|
||||
defaultIPv4 = "127.0.0.1"
|
||||
defaultIPv6 = "::1"
|
||||
)
|
||||
|
||||
// challengeServers offer common functionality to start up and shutdown.
|
||||
type challengeServer interface {
|
||||
ListenAndServe() error
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
// ChallSrv is a multi-purpose challenge server. Each ChallSrv may have one or
|
||||
// more ACME challenges it provides servers for. It is safe to use concurrently.
|
||||
type ChallSrv struct {
|
||||
log *log.Logger
|
||||
|
||||
// servers are the individual challenge server listeners started in New() and
|
||||
// closed in Shutdown().
|
||||
servers []challengeServer
|
||||
|
||||
// challMu is a RWMutex used to control concurrent updates to the challenge
|
||||
// response data maps below.
|
||||
challMu sync.RWMutex
|
||||
|
||||
// requestHistory is a map from hostname to a map of event type to a list of
|
||||
// sequential request events
|
||||
requestHistory map[string]map[RequestEventType][]RequestEvent
|
||||
|
||||
// httpOne is a map of token values to key authorizations used for HTTP-01
|
||||
// responses.
|
||||
httpOne map[string]string
|
||||
|
||||
// dnsOne is a map of DNS host values to key authorizations used for DNS-01
|
||||
// responses.
|
||||
dnsOne map[string][]string
|
||||
|
||||
// dnsMocks holds mock DNS data used to respond to DNS queries other than
|
||||
// DNS-01 TXT challenge lookups.
|
||||
dnsMocks mockDNSData
|
||||
|
||||
// tlsALPNOne is a map of token values to key authorizations used for TLS-ALPN-01
|
||||
// responses.
|
||||
tlsALPNOne map[string]string
|
||||
|
||||
// redirects is a map of paths to URLs. HTTP challenge servers respond to
|
||||
// requests for these paths with a 301 to the corresponding URL.
|
||||
redirects map[string]string
|
||||
}
|
||||
|
||||
// mockDNSData holds mock responses for DNS A, AAAA, and CAA lookups.
|
||||
type mockDNSData struct {
|
||||
// The IPv4 address used for all A record responses that don't match a host in
|
||||
// aRecords.
|
||||
defaultIPv4 string
|
||||
// The IPv6 address used for all AAAA record responses that don't match a host
|
||||
// in aaaaRecords.
|
||||
defaultIPv6 string
|
||||
// A map of host to IPv4 addresses in string form for A record responses.
|
||||
aRecords map[string][]string
|
||||
// A map of host to IPv6 addresses in string form for AAAA record responses.
|
||||
aaaaRecords map[string][]string
|
||||
// A map of host to CAA policies for CAA responses.
|
||||
caaRecords map[string][]MockCAAPolicy
|
||||
// A map of host to CNAME records.
|
||||
cnameRecords map[string]string
|
||||
// A map of hostnames that should receive a SERVFAIL response for all queries.
|
||||
servFailRecords map[string]bool
|
||||
}
|
||||
|
||||
// MockCAAPolicy holds a tag and a value for a CAA record. See
|
||||
// https://tools.ietf.org/html/rfc6844
|
||||
type MockCAAPolicy struct {
|
||||
Tag string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Config holds challenge server configuration
|
||||
type Config struct {
|
||||
Log *log.Logger
|
||||
// HTTPOneAddrs are the HTTP-01 challenge server bind addresses/ports
|
||||
HTTPOneAddrs []string
|
||||
// HTTPSOneAddrs are the HTTPS HTTP-01 challenge server bind addresses/ports
|
||||
HTTPSOneAddrs []string
|
||||
// DOHAddrs are the DOH challenge server bind addresses/ports
|
||||
DOHAddrs []string
|
||||
// DNSOneAddrs are the DNS-01 challenge server bind addresses/ports
|
||||
DNSOneAddrs []string
|
||||
// TLSALPNOneAddrs are the TLS-ALPN-01 challenge server bind addresses/ports
|
||||
TLSALPNOneAddrs []string
|
||||
|
||||
// DOHCert is required if DOHAddrs is nonempty.
|
||||
DOHCert string
|
||||
// DOHCertKey is required if DOHAddrs is nonempty.
|
||||
DOHCertKey string
|
||||
}
|
||||
|
||||
// validate checks that a challenge server Config is valid. To be valid it must
|
||||
// specify a bind address for at least one challenge type. If there is no
|
||||
// configured log in the config a default is provided.
|
||||
func (c *Config) validate() error {
|
||||
// There needs to be at least one challenge type with a bind address
|
||||
if len(c.HTTPOneAddrs) < 1 &&
|
||||
len(c.HTTPSOneAddrs) < 1 &&
|
||||
len(c.DNSOneAddrs) < 1 &&
|
||||
len(c.TLSALPNOneAddrs) < 1 {
|
||||
return fmt.Errorf(
|
||||
"config must specify at least one HTTPOneAddrs entry, one HTTPSOneAddr " +
|
||||
"entry, one DOHAddrs, one DNSOneAddrs entry, or one TLSALPNOneAddrs entry")
|
||||
}
|
||||
// If there is no configured log make a default with a prefix
|
||||
if c.Log == nil {
|
||||
c.Log = log.New(os.Stdout, "challtestsrv - ", log.LstdFlags)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New constructs and returns a new ChallSrv instance with the given Config.
|
||||
func New(config Config) (*ChallSrv, error) {
|
||||
// Validate the provided configuration
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challSrv := &ChallSrv{
|
||||
log: config.Log,
|
||||
requestHistory: make(map[string]map[RequestEventType][]RequestEvent),
|
||||
httpOne: make(map[string]string),
|
||||
dnsOne: make(map[string][]string),
|
||||
tlsALPNOne: make(map[string]string),
|
||||
redirects: make(map[string]string),
|
||||
dnsMocks: mockDNSData{
|
||||
defaultIPv4: defaultIPv4,
|
||||
defaultIPv6: defaultIPv6,
|
||||
aRecords: make(map[string][]string),
|
||||
aaaaRecords: make(map[string][]string),
|
||||
caaRecords: make(map[string][]MockCAAPolicy),
|
||||
cnameRecords: make(map[string]string),
|
||||
servFailRecords: make(map[string]bool),
|
||||
},
|
||||
}
|
||||
|
||||
// If there are HTTP-01 addresses configured, create HTTP-01 servers with
|
||||
// HTTPS disabled.
|
||||
for _, address := range config.HTTPOneAddrs {
|
||||
challSrv.log.Printf("Creating HTTP-01 challenge server on %s\n", address)
|
||||
challSrv.servers = append(challSrv.servers, httpOneServer(address, challSrv, false))
|
||||
}
|
||||
|
||||
// If there are HTTPS HTTP-01 addresses configured, create HTTP-01 servers
|
||||
// with HTTPS enabled.
|
||||
for _, address := range config.HTTPSOneAddrs {
|
||||
challSrv.log.Printf("Creating HTTPS HTTP-01 challenge server on %s\n", address)
|
||||
challSrv.servers = append(challSrv.servers, httpOneServer(address, challSrv, true))
|
||||
}
|
||||
|
||||
// If there are DNS-01 addresses configured, create DNS-01 servers
|
||||
for _, address := range config.DNSOneAddrs {
|
||||
challSrv.log.Printf("Creating TCP and UDP DNS-01 challenge server on %s\n", address)
|
||||
challSrv.servers = append(challSrv.servers,
|
||||
dnsOneServer(address, challSrv.dnsHandler)...)
|
||||
}
|
||||
|
||||
for _, address := range config.DOHAddrs {
|
||||
challSrv.log.Printf("Creating DoH server on %s\n", address)
|
||||
s, err := dohServer(address, config.DOHCert, config.DOHCertKey, http.HandlerFunc(challSrv.dohHandler))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
challSrv.servers = append(challSrv.servers, s)
|
||||
}
|
||||
|
||||
// If there are TLS-ALPN-01 addresses configured, create TLS-ALPN-01 servers
|
||||
for _, address := range config.TLSALPNOneAddrs {
|
||||
challSrv.log.Printf("Creating TLS-ALPN-01 challenge server on %s\n", address)
|
||||
challSrv.servers = append(challSrv.servers, tlsALPNOneServer(address, challSrv))
|
||||
}
|
||||
|
||||
return challSrv, nil
|
||||
}
|
||||
|
||||
// Run starts each of the ChallSrv's challengeServers.
|
||||
func (s *ChallSrv) Run() {
|
||||
s.log.Printf("Starting challenge servers")
|
||||
|
||||
// Start each server in their own dedicated Go routine
|
||||
for _, srv := range s.servers {
|
||||
go func(srv challengeServer) {
|
||||
err := srv.ListenAndServe()
|
||||
if err != nil && !strings.Contains(err.Error(), "Server closed") {
|
||||
s.log.Print(err)
|
||||
}
|
||||
}(srv)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops each of the ChallSrv's challengeServers.
|
||||
func (s *ChallSrv) Shutdown() {
|
||||
for _, srv := range s.servers {
|
||||
if err := srv.Shutdown(); err != nil {
|
||||
s.log.Printf("err in Shutdown(): %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package challtestsrv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// mockSOA returns a mock DNS SOA record with fake data.
|
||||
func mockSOA() *dns.SOA {
|
||||
return &dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "challtestsrv.invalid.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ns: "ns.challtestsrv.invalid.",
|
||||
Mbox: "master.challtestsrv.invalid.",
|
||||
Serial: 1,
|
||||
Refresh: 1,
|
||||
Retry: 1,
|
||||
Expire: 1,
|
||||
Minttl: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// dnsAnswerFunc is a function that accepts a DNS question and returns one or
|
||||
// more RRs for the response.
|
||||
type dnsAnswerFunc func(question dns.Question) []dns.RR
|
||||
|
||||
// cnameAnswers is a dnsAnswerFunc that creates CNAME RR's for the given question
|
||||
// using the ChallSrv's dns mock data. If there is no mock CNAME data for the
|
||||
// given hostname in the question no RR's will be returned.
|
||||
func (s *ChallSrv) cnameAnswers(q dns.Question) []dns.RR {
|
||||
var records []dns.RR
|
||||
|
||||
if value := s.GetDNSCNAMERecord(q.Name); value != "" {
|
||||
record := &dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Target: value,
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
// txtAnswers is a dnsAnswerFunc that creates TXT RR's for the given question
|
||||
// using the ChallSrv's dns mock data. If there is no mock TXT data for the
|
||||
// given hostname in the question no RR's will be returned.
|
||||
func (s *ChallSrv) txtAnswers(q dns.Question) []dns.RR {
|
||||
var records []dns.RR
|
||||
values := s.GetDNSOneChallenge(q.Name)
|
||||
for _, resp := range values {
|
||||
record := &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Txt: []string{resp},
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// aAnswers is a dnsAnswerFunc that creates A RR's for the given question using
|
||||
// the ChallSrv's dns mock data. If there is not a mock ipv4 A response added
|
||||
// for the given hostname in the question the default IPv4 address will be used
|
||||
// for the response.
|
||||
func (s *ChallSrv) aAnswers(q dns.Question) []dns.RR {
|
||||
var records []dns.RR
|
||||
// Don't answer any questions for IP addresses with a fakeDNS response.
|
||||
// These queries are invalid!
|
||||
if ip := net.ParseIP(q.Name); ip != nil {
|
||||
return records
|
||||
}
|
||||
values := s.GetDNSARecord(q.Name)
|
||||
if defaultIPv4 := s.GetDefaultDNSIPv4(); len(values) == 0 && defaultIPv4 != "" {
|
||||
values = []string{defaultIPv4}
|
||||
}
|
||||
for _, resp := range values {
|
||||
ipAddr := net.ParseIP(resp)
|
||||
if ipAddr == nil || ipAddr.To4() == nil {
|
||||
// If the mock data isn't a valid IPv4 address, don't use it.
|
||||
continue
|
||||
}
|
||||
record := &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: ipAddr,
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// aaaaAnswers is a dnsAnswerFunc that creates AAAA RR's for the given question
|
||||
// using the ChallSrv's dns mock data. If there is not a mock IPv6 AAAA response
|
||||
// added for the given hostname in the question the default IPv6 address will be
|
||||
// used for the response.
|
||||
func (s *ChallSrv) aaaaAnswers(q dns.Question) []dns.RR {
|
||||
var records []dns.RR
|
||||
values := s.GetDNSAAAARecord(q.Name)
|
||||
if defaultIPv6 := s.GetDefaultDNSIPv6(); len(values) == 0 && defaultIPv6 != "" {
|
||||
values = []string{defaultIPv6}
|
||||
}
|
||||
for _, resp := range values {
|
||||
ipAddr := net.ParseIP(resp)
|
||||
if ipAddr == nil || ipAddr.To4() != nil {
|
||||
// If the mock data isn't a valid IPv6 address, don't use it.
|
||||
continue
|
||||
}
|
||||
record := &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
AAAA: ipAddr,
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// caaAnswers is a dnsAnswerFunc that creates CAA RR's for the given question
|
||||
// using the ChallSrv's dns mock data. If there is not a mock CAA response
|
||||
// added for the given hostname in the question no RRs will be returned.
|
||||
func (s *ChallSrv) caaAnswers(q dns.Question) []dns.RR {
|
||||
var records []dns.RR
|
||||
values := s.GetDNSCAARecord(q.Name)
|
||||
for _, resp := range values {
|
||||
record := &dns.CAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeCAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Tag: resp.Tag,
|
||||
Value: resp.Value,
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
type writeMsg interface {
|
||||
WriteMsg(*dns.Msg) error
|
||||
}
|
||||
|
||||
type dnsToHTTPWriter struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (d *dnsToHTTPWriter) WriteMsg(m *dns.Msg) error {
|
||||
d.Header().Set("Content-Type", "application/dns-message")
|
||||
d.WriteHeader(http.StatusOK)
|
||||
b, err := m.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// dohHandler handles a DoH request by POST only.
|
||||
func (s *ChallSrv) dohHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
msg := new(dns.Msg)
|
||||
err = msg.Unpack(body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintln(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.dnsHandlerInner(&dnsToHTTPWriter{w}, msg)
|
||||
}
|
||||
|
||||
// dnsHandler is a miekg/dns handler that can process a dns.Msg request and
|
||||
// write a response to the provided dns.ResponseWriter. TXT, A, AAAA, CNAME,
|
||||
// and CAA queries types are supported and answered using the ChallSrv's mock
|
||||
// DNS data. A host that is aliased by a CNAME record will follow that alias
|
||||
// one level and return the requested record types for that alias' target
|
||||
func (s *ChallSrv) dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
|
||||
s.dnsHandlerInner(w, r)
|
||||
}
|
||||
|
||||
func (s *ChallSrv) dnsHandlerInner(w writeMsg, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Compress = false
|
||||
|
||||
// For each question, add answers based on the type of question
|
||||
for _, q := range r.Question {
|
||||
s.AddRequestEvent(DNSRequestEvent{
|
||||
Question: q,
|
||||
})
|
||||
|
||||
// If there is a ServFail mock set then ignore the question and set the
|
||||
// SERVFAIL rcode and continue.
|
||||
if s.GetDNSServFailRecord(q.Name) {
|
||||
m.SetRcode(r, dns.RcodeServerFailure)
|
||||
continue
|
||||
}
|
||||
|
||||
// If a CNAME exists for the question include the CNAME record and modify
|
||||
// the question to instead lookup based on that CNAME's target
|
||||
if cname := s.GetDNSCNAMERecord(q.Name); cname != "" {
|
||||
cnameRecords := s.cnameAnswers(q)
|
||||
m.Answer = append(m.Answer, cnameRecords...)
|
||||
|
||||
q = dns.Question{Name: cname, Qtype: q.Qtype}
|
||||
}
|
||||
|
||||
var answerFunc dnsAnswerFunc
|
||||
switch q.Qtype {
|
||||
case dns.TypeCNAME:
|
||||
answerFunc = s.cnameAnswers
|
||||
case dns.TypeTXT:
|
||||
answerFunc = s.txtAnswers
|
||||
case dns.TypeA:
|
||||
answerFunc = s.aAnswers
|
||||
case dns.TypeAAAA:
|
||||
answerFunc = s.aaaaAnswers
|
||||
case dns.TypeCAA:
|
||||
answerFunc = s.caaAnswers
|
||||
default:
|
||||
m.SetRcode(r, dns.RcodeNotImplemented)
|
||||
}
|
||||
|
||||
if answerFunc == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if records := answerFunc(q); len(records) > 0 {
|
||||
m.Answer = append(m.Answer, records...)
|
||||
}
|
||||
}
|
||||
|
||||
m.Ns = append(m.Ns, mockSOA())
|
||||
_ = w.WriteMsg(m)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package challtestsrv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// AddDNSOneChallenge adds a TXT record for the given host with the given
|
||||
// content.
|
||||
func (s *ChallSrv) AddDNSOneChallenge(host, content string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
s.dnsOne[host] = append(s.dnsOne[host], content)
|
||||
}
|
||||
|
||||
// DeleteDNSOneChallenge deletes a TXT record for the given host.
|
||||
func (s *ChallSrv) DeleteDNSOneChallenge(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
delete(s.dnsOne, host)
|
||||
}
|
||||
|
||||
// GetDNSOneChallenge returns a slice of TXT record values for the given host.
|
||||
// If the host does not exist in the challenge response data then nil is
|
||||
// returned.
|
||||
func (s *ChallSrv) GetDNSOneChallenge(host string) []string {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
return s.dnsOne[host]
|
||||
}
|
||||
|
||||
type dnsHandler func(dns.ResponseWriter, *dns.Msg)
|
||||
|
||||
// dnsOneServer creates an ACME DNS-01 challenge server. The provided dns
|
||||
// handler will be registered with the `miekg/dns` package to
|
||||
// handle DNS requests. Because the DNS server runs both a UDP and a TCP
|
||||
// listener two `server` objects are returned.
|
||||
func dnsOneServer(address string, handler dnsHandler) []challengeServer {
|
||||
// Register the dnsHandler
|
||||
dns.HandleFunc(".", handler)
|
||||
// Create a UDP DNS server
|
||||
udpServer := &dns.Server{
|
||||
Addr: address,
|
||||
Net: "udp",
|
||||
ReadTimeout: time.Second,
|
||||
WriteTimeout: time.Second,
|
||||
}
|
||||
// Create a TCP DNS server
|
||||
tcpServer := &dns.Server{
|
||||
Addr: address,
|
||||
Net: "tcp",
|
||||
ReadTimeout: time.Second,
|
||||
WriteTimeout: time.Second,
|
||||
}
|
||||
return []challengeServer{udpServer, tcpServer}
|
||||
}
|
||||
|
||||
type doh struct {
|
||||
*http.Server
|
||||
tlsCert, tlsCertKey string
|
||||
}
|
||||
|
||||
func (s *doh) Shutdown() error {
|
||||
return s.Server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func (s *doh) ListenAndServe() error {
|
||||
return s.Server.ListenAndServeTLS(s.tlsCert, s.tlsCertKey)
|
||||
}
|
||||
|
||||
// dohServer creates a DoH server.
|
||||
func dohServer(address string, tlsCert, tlsCertKey string, handler http.Handler) (challengeServer, error) {
|
||||
return &doh{
|
||||
&http.Server{
|
||||
Handler: handler,
|
||||
Addr: address,
|
||||
ReadTimeout: time.Second,
|
||||
WriteTimeout: time.Second,
|
||||
},
|
||||
tlsCert,
|
||||
tlsCertKey,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package challtestsrv
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// RequestEventType indicates what type of event occurred.
|
||||
type RequestEventType int
|
||||
|
||||
const (
|
||||
// HTTP requests
|
||||
HTTPRequestEventType RequestEventType = iota
|
||||
// DNS requests
|
||||
DNSRequestEventType
|
||||
// TLS-ALPN-01 requests
|
||||
TLSALPNRequestEventType
|
||||
)
|
||||
|
||||
// A RequestEvent is anything that can identify its RequestEventType and a key
|
||||
// for storing the request event in the history.
|
||||
type RequestEvent interface {
|
||||
Type() RequestEventType
|
||||
Key() string
|
||||
}
|
||||
|
||||
// HTTPRequestEvent corresponds to an HTTP request received by a httpOneServer.
|
||||
// It implements the RequestEvent interface.
|
||||
type HTTPRequestEvent struct {
|
||||
// The full request URL (path and query arguments)
|
||||
URL string
|
||||
// The Host header from the request
|
||||
Host string
|
||||
// Whether the request was received over HTTPS or HTTP
|
||||
HTTPS bool
|
||||
// The ServerName from the ClientHello. May be empty if there was no SNI or if
|
||||
// the request was not HTTPS
|
||||
ServerName string
|
||||
}
|
||||
|
||||
// HTTPRequestEvents always have type HTTPRequestEventType
|
||||
func (e HTTPRequestEvent) Type() RequestEventType {
|
||||
return HTTPRequestEventType
|
||||
}
|
||||
|
||||
// HTTPRequestEvents use the HTTP Host as the storage key. Any explicit port
|
||||
// will be removed.
|
||||
func (e HTTPRequestEvent) Key() string {
|
||||
if h, _, err := net.SplitHostPort(e.Host); err == nil {
|
||||
return h
|
||||
}
|
||||
return e.Host
|
||||
}
|
||||
|
||||
// DNSRequestEvent corresponds to a DNS request received by a dnsOneServer. It
|
||||
// implements the RequestEvent interface.
|
||||
type DNSRequestEvent struct {
|
||||
// The DNS question received.
|
||||
Question dns.Question
|
||||
}
|
||||
|
||||
// DNSRequestEvents always have type DNSRequestEventType
|
||||
func (e DNSRequestEvent) Type() RequestEventType {
|
||||
return DNSRequestEventType
|
||||
}
|
||||
|
||||
// DNSRequestEvents use the Question Name as the storage key. Any trailing `.`
|
||||
// in the question name is removed.
|
||||
func (e DNSRequestEvent) Key() string {
|
||||
key := e.Question.Name
|
||||
if strings.HasSuffix(key, ".") {
|
||||
key = strings.TrimSuffix(key, ".")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// TLSALPNRequestEvent corresponds to a TLS request received by
|
||||
// a tlsALPNOneServer. It implements the RequestEvent interface.
|
||||
type TLSALPNRequestEvent struct {
|
||||
// ServerName from the TLS Client Hello.
|
||||
ServerName string
|
||||
// SupportedProtos from the TLS Client Hello.
|
||||
SupportedProtos []string
|
||||
}
|
||||
|
||||
// TLSALPNRequestEvents always have type TLSALPNRequestEventType
|
||||
func (e TLSALPNRequestEvent) Type() RequestEventType {
|
||||
return TLSALPNRequestEventType
|
||||
}
|
||||
|
||||
// TLSALPNRequestEvents use the SNI value as the storage key
|
||||
func (e TLSALPNRequestEvent) Key() string {
|
||||
return e.ServerName
|
||||
}
|
||||
|
||||
// AddRequestEvent adds a RequestEvent to the server's request history. It is
|
||||
// appeneded to a list of RequestEvents indexed by the event's Type().
|
||||
func (s *ChallSrv) AddRequestEvent(event RequestEvent) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
|
||||
typ := event.Type()
|
||||
host := event.Key()
|
||||
if s.requestHistory[host] == nil {
|
||||
s.requestHistory[host] = make(map[RequestEventType][]RequestEvent)
|
||||
}
|
||||
s.requestHistory[host][typ] = append(s.requestHistory[host][typ], event)
|
||||
}
|
||||
|
||||
// RequestHistory returns the server's request history for the given hostname
|
||||
// and event type.
|
||||
func (s *ChallSrv) RequestHistory(hostname string, typ RequestEventType) []RequestEvent {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
|
||||
if hostEvents, ok := s.requestHistory[hostname]; ok {
|
||||
return hostEvents[typ]
|
||||
}
|
||||
return []RequestEvent{}
|
||||
}
|
||||
|
||||
// ClearRequestHistory clears the server's request history for the given
|
||||
// hostname and event type.
|
||||
func (s *ChallSrv) ClearRequestHistory(hostname string, typ RequestEventType) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
|
||||
if hostEvents, ok := s.requestHistory[hostname]; ok {
|
||||
hostEvents[typ] = []RequestEvent{}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package challtestsrv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// wellKnownPath is the IANA registered ACME HTTP-01 challenge path. See
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-9.2
|
||||
const wellKnownPath = "/.well-known/acme-challenge/"
|
||||
|
||||
// cert is a self-signed certificate issued at startup for the HTTPS HTTP-01
|
||||
// server.
|
||||
var cert = selfSignedCert()
|
||||
|
||||
// selfSignedCert issues a self-signed CA certificate to use as the leaf
|
||||
// certificate for an HTTPS server serving HTTP-01 challenges. This certificate
|
||||
// will not be trusted by normal TLS clients but HTTP-01 redirects to HTTPS will
|
||||
// ignore certificate validation.
|
||||
func selfSignedCert() tls.Certificate {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to generate HTTPS ECDSA key: %v", err))
|
||||
}
|
||||
|
||||
serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to generate HTTPS cert serial number: %v", err))
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "challenge test server",
|
||||
},
|
||||
SerialNumber: serial,
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to issue HTTPS cert: %v", err))
|
||||
}
|
||||
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{der},
|
||||
PrivateKey: key,
|
||||
}
|
||||
}
|
||||
|
||||
// AddHTTPOneChallenge adds a new HTTP-01 challenge for the given token and
|
||||
// content.
|
||||
func (s *ChallSrv) AddHTTPOneChallenge(token, content string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
s.httpOne[token] = content
|
||||
}
|
||||
|
||||
// DeleteHTTPOneChallenge deletes a given HTTP-01 challenge token.
|
||||
func (s *ChallSrv) DeleteHTTPOneChallenge(token string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
delete(s.httpOne, token)
|
||||
}
|
||||
|
||||
// GetHTTPOneChallenge returns the HTTP-01 challenge content for the given token
|
||||
// (if it exists) and a true bool. If the token does not exist then an empty
|
||||
// string and a false bool are returned.
|
||||
func (s *ChallSrv) GetHTTPOneChallenge(token string) (string, bool) {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
content, present := s.httpOne[token]
|
||||
return content, present
|
||||
}
|
||||
|
||||
// AddHTTPRedirect adds a redirect for the given path to the given URL.
|
||||
func (s *ChallSrv) AddHTTPRedirect(path, targetURL string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
s.redirects[path] = targetURL
|
||||
}
|
||||
|
||||
// DeleteHTTPRedirect deletes a redirect for the given path.
|
||||
func (s *ChallSrv) DeleteHTTPRedirect(path string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
delete(s.redirects, path)
|
||||
}
|
||||
|
||||
// GetHTTPRedirect returns the redirect target for the given path
|
||||
// (if it exists) and a true bool. If the path does not have a redirect target
|
||||
// then an empty string and a false bool are returned.
|
||||
func (s *ChallSrv) GetHTTPRedirect(path string) (string, bool) {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
targetURL, present := s.redirects[path]
|
||||
return targetURL, present
|
||||
}
|
||||
|
||||
// ServeHTTP handles an HTTP request. If the request path has the ACME HTTP-01
|
||||
// challenge well known prefix as a prefix and the token specified is known,
|
||||
// then the challenge response contents are returned.
|
||||
func (s *ChallSrv) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
requestPath := r.URL.Path
|
||||
|
||||
serverName := ""
|
||||
if r.TLS != nil {
|
||||
serverName = r.TLS.ServerName
|
||||
}
|
||||
|
||||
s.AddRequestEvent(HTTPRequestEvent{
|
||||
URL: r.URL.String(),
|
||||
Host: r.Host,
|
||||
HTTPS: r.TLS != nil,
|
||||
ServerName: serverName,
|
||||
})
|
||||
|
||||
// If the request was not over HTTPS and we have a redirect, serve it.
|
||||
// Redirects are ignored over HTTPS so we can easily do an HTTP->HTTPS
|
||||
// redirect for a token path without creating a loop.
|
||||
if redirectTarget, found := s.GetHTTPRedirect(requestPath); found && r.TLS == nil {
|
||||
http.Redirect(w, r, redirectTarget, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(requestPath, wellKnownPath) {
|
||||
token := requestPath[len(wellKnownPath):]
|
||||
if auth, found := s.GetHTTPOneChallenge(token); found {
|
||||
fmt.Fprintf(w, "%s", auth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// challHTTPServer is a *http.Server that has a Shutdown() func that doesn't
|
||||
// take a context argument. This lets us treat the HTTP server the same as the
|
||||
// DNS-01 servers (which use a `dns.Server` that has `Shutdown()` with no
|
||||
// context arg) by having an http.Server that implements the challengeServer
|
||||
// interface.
|
||||
type challHTTPServer struct {
|
||||
*http.Server
|
||||
}
|
||||
|
||||
// ListenAndServe for a challHTTPServer will call the underlying http.Server's
|
||||
// ListenAndServeTLS if the server has a non-nil TLSConfig, otherwise it will
|
||||
// use the underlying http.Server's ListenAndServe(). This allows for
|
||||
// a challHTTPServer to be both a normal HTTP based HTTP-01 challenge response
|
||||
// server in one configuration (nil TLSConfig) and an HTTPS based HTTP-01
|
||||
// challenge response server useful for redirect targets in another
|
||||
// configuration.
|
||||
func (c challHTTPServer) ListenAndServe() error {
|
||||
if c.Server.TLSConfig != nil {
|
||||
// This will use the certificate and key from TLSConfig.
|
||||
return c.Server.ListenAndServeTLS("", "")
|
||||
}
|
||||
// Otherwise use HTTP
|
||||
return c.Server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (c challHTTPServer) Shutdown() error {
|
||||
return c.Server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
// httpOneServer creates an ACME HTTP-01 challenge server. The
|
||||
// server's handler will return configured HTTP-01 challenge responses for
|
||||
// tokens that have been added to the challenge server. If HTTPS is true the
|
||||
// resulting challengeServer will run a HTTPS server with a self-signed
|
||||
// certificate useful for HTTP-01 -> HTTPS HTTP-01 redirect responses. If HTTPS
|
||||
// is false the resulting challengeServer will run an HTTP server.
|
||||
func httpOneServer(address string, handler http.Handler, https bool) challengeServer {
|
||||
// If HTTPS is requested build a TLS Config that uses the self-signed
|
||||
// certificate generated at startup.
|
||||
var tlsConfig *tls.Config
|
||||
if https {
|
||||
tlsConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
// Create an HTTP Server for HTTP-01 challenges
|
||||
srv := &http.Server{
|
||||
Addr: address,
|
||||
Handler: handler,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
srv.SetKeepAlivesEnabled(false)
|
||||
return challHTTPServer{srv}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package challtestsrv
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// SetDefaultDNSIPv4 sets the default IPv4 address used for A query responses
|
||||
// that don't match hosts added with AddDNSARecord. Use "" to disable default
|
||||
// A query responses.
|
||||
func (s *ChallSrv) SetDefaultDNSIPv4(addr string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
s.dnsMocks.defaultIPv4 = addr
|
||||
}
|
||||
|
||||
// SetDefaultDNSIPv6 sets the default IPv6 address used for AAAA query responses
|
||||
// that don't match hosts added with AddDNSAAAARecord. Use "" to disable default
|
||||
// AAAA query responses.
|
||||
func (s *ChallSrv) SetDefaultDNSIPv6(addr string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
s.dnsMocks.defaultIPv6 = addr
|
||||
}
|
||||
|
||||
// GetDefaultDNSIPv4 gets the default IPv4 address used for A query responses
|
||||
// (in string form), or an empty string if no default is being used.
|
||||
func (s *ChallSrv) GetDefaultDNSIPv4() string {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
return s.dnsMocks.defaultIPv4
|
||||
}
|
||||
|
||||
// GetDefaultDNSIPv6 gets the default IPv6 address used for AAAA query responses
|
||||
// (in string form), or an empty string if no default is being used.
|
||||
func (s *ChallSrv) GetDefaultDNSIPv6() string {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
return s.dnsMocks.defaultIPv6
|
||||
}
|
||||
|
||||
// AddDNSCNAMERecord sets a CNAME record that will be used like an alias when
|
||||
// querying for other DNS records for the given host.
|
||||
func (s *ChallSrv) AddDNSCNAMERecord(host string, value string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
value = dns.Fqdn(value)
|
||||
s.dnsMocks.cnameRecords[host] = value
|
||||
}
|
||||
|
||||
// GetDNSCNAMERecord returns a target host if a CNAME is set for the querying
|
||||
// host and an empty string otherwise.
|
||||
func (s *ChallSrv) GetDNSCNAMERecord(host string) string {
|
||||
s.challMu.RLock()
|
||||
host = dns.Fqdn(host)
|
||||
defer s.challMu.RUnlock()
|
||||
return s.dnsMocks.cnameRecords[host]
|
||||
}
|
||||
|
||||
// DeleteDNSCAMERecord deletes any CNAME alias set for the given host.
|
||||
func (s *ChallSrv) DeleteDNSCNAMERecord(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
delete(s.dnsMocks.cnameRecords, host)
|
||||
}
|
||||
|
||||
// AddDNSARecord adds IPv4 addresses that will be returned when querying for
|
||||
// A records for the given host.
|
||||
func (s *ChallSrv) AddDNSARecord(host string, addresses []string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
s.dnsMocks.aRecords[host] = append(s.dnsMocks.aRecords[host], addresses...)
|
||||
}
|
||||
|
||||
// DeleteDNSARecord deletes any IPv4 addresses that will be returned when
|
||||
// querying for A records for the given host.record for the given host.
|
||||
func (s *ChallSrv) DeleteDNSARecord(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
delete(s.dnsMocks.aRecords, host)
|
||||
}
|
||||
|
||||
// GetDNSARecord returns a slice of IPv4 addresses (in string form) that will be
|
||||
// returned when querying for A records for the given host.
|
||||
func (s *ChallSrv) GetDNSARecord(host string) []string {
|
||||
s.challMu.RLock()
|
||||
host = dns.Fqdn(host)
|
||||
defer s.challMu.RUnlock()
|
||||
return s.dnsMocks.aRecords[host]
|
||||
}
|
||||
|
||||
// AddDNSAAAARecord adds IPv6 addresses that will be returned when querying for
|
||||
// AAAA records for the given host.
|
||||
func (s *ChallSrv) AddDNSAAAARecord(host string, addresses []string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
s.dnsMocks.aaaaRecords[host] = append(s.dnsMocks.aaaaRecords[host], addresses...)
|
||||
}
|
||||
|
||||
// DeleteDNSAAAARecord deletes any IPv6 addresses that will be returned when
|
||||
// querying for A records for the given host.
|
||||
func (s *ChallSrv) DeleteDNSAAAARecord(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
delete(s.dnsMocks.aaaaRecords, host)
|
||||
}
|
||||
|
||||
// GetDNSAAAARecord returns a slice of IPv6 addresses (in string form) that will
|
||||
// be returned when querying for A records for the given host.
|
||||
func (s *ChallSrv) GetDNSAAAARecord(host string) []string {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
host = dns.Fqdn(host)
|
||||
return s.dnsMocks.aaaaRecords[host]
|
||||
}
|
||||
|
||||
// AddDNSCAARecord adds mock CAA records that will be returned when querying
|
||||
// CAA for the given host.
|
||||
func (s *ChallSrv) AddDNSCAARecord(host string, policies []MockCAAPolicy) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
s.dnsMocks.caaRecords[host] = append(s.dnsMocks.caaRecords[host], policies...)
|
||||
}
|
||||
|
||||
// DeleteDNSCAARecord deletes any CAA policies that will be returned when
|
||||
// querying CAA for the given host.
|
||||
func (s *ChallSrv) DeleteDNSCAARecord(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
delete(s.dnsMocks.caaRecords, host)
|
||||
}
|
||||
|
||||
// GetDNSCAARecord returns a slice of mock CAA policies that will
|
||||
// be returned when querying CAA for the given host.
|
||||
func (s *ChallSrv) GetDNSCAARecord(host string) []MockCAAPolicy {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
host = dns.Fqdn(host)
|
||||
return s.dnsMocks.caaRecords[host]
|
||||
}
|
||||
|
||||
// AddDNSServFailRecord configures the chall srv to return SERVFAIL responses
|
||||
// for all queries for the given host.
|
||||
func (s *ChallSrv) AddDNSServFailRecord(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
s.dnsMocks.servFailRecords[host] = true
|
||||
}
|
||||
|
||||
// DeleteDNSServFailRecord configures the chall srv to no longer return SERVFAIL
|
||||
// responses for all queries for the given host.
|
||||
func (s *ChallSrv) DeleteDNSServFailRecord(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
host = dns.Fqdn(host)
|
||||
delete(s.dnsMocks.servFailRecords, host)
|
||||
}
|
||||
|
||||
// GetDNSServFailRecord returns true when the chall srv has been configured with
|
||||
// AddDNSServFailRecord to return SERVFAIL for all queries to the given host.
|
||||
func (s *ChallSrv) GetDNSServFailRecord(host string) bool {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
host = dns.Fqdn(host)
|
||||
return s.dnsMocks.servFailRecords[host]
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package challtestsrv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ALPN protocol ID for TLS-ALPN-01 challenge
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
|
||||
const ACMETLS1Protocol = "acme-tls/1"
|
||||
|
||||
// IDPeAcmeIdentifier is the identifier defined in
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-04#section-5.1
|
||||
// id-pe OID + 31 (acmeIdentifier)
|
||||
var IDPeAcmeIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
||||
|
||||
// AddTLSALPNChallenge adds a new TLS-ALPN-01 key authorization for the given host
|
||||
func (s *ChallSrv) AddTLSALPNChallenge(host, content string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
s.tlsALPNOne[host] = content
|
||||
}
|
||||
|
||||
// DeleteTLSALPNChallenge deletes the key authorization for a given host
|
||||
func (s *ChallSrv) DeleteTLSALPNChallenge(host string) {
|
||||
s.challMu.Lock()
|
||||
defer s.challMu.Unlock()
|
||||
delete(s.tlsALPNOne, host)
|
||||
}
|
||||
|
||||
// GetTLSALPNChallenge checks the s.tlsALPNOne map for the given host.
|
||||
// If it is present it returns the key authorization and true, if not
|
||||
// it returns an empty string and false.
|
||||
func (s *ChallSrv) GetTLSALPNChallenge(host string) (string, bool) {
|
||||
s.challMu.RLock()
|
||||
defer s.challMu.RUnlock()
|
||||
content, present := s.tlsALPNOne[host]
|
||||
return content, present
|
||||
}
|
||||
|
||||
func (s *ChallSrv) ServeChallengeCertFunc(k *ecdsa.PrivateKey) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
s.AddRequestEvent(TLSALPNRequestEvent{
|
||||
ServerName: hello.ServerName,
|
||||
SupportedProtos: hello.SupportedProtos,
|
||||
})
|
||||
if len(hello.SupportedProtos) != 1 || hello.SupportedProtos[0] != ACMETLS1Protocol {
|
||||
return nil, fmt.Errorf(
|
||||
"ALPN failed, ClientHelloInfo.SupportedProtos: %s",
|
||||
hello.SupportedProtos)
|
||||
}
|
||||
|
||||
ka, found := s.GetTLSALPNChallenge(hello.ServerName)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("unknown ClientHelloInfo.ServerName: %s", hello.ServerName)
|
||||
}
|
||||
|
||||
kaHash := sha256.Sum256([]byte(ka))
|
||||
extValue, err := asn1.Marshal(kaHash[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed marshalling hash OCTET STRING: %s", err)
|
||||
}
|
||||
certTmpl := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1729),
|
||||
DNSNames: []string{hello.ServerName},
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: IDPeAcmeIdentifier,
|
||||
Critical: true,
|
||||
Value: extValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, &certTmpl, k.Public(), k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating challenge certificate: %s", err)
|
||||
}
|
||||
return &tls.Certificate{
|
||||
Certificate: [][]byte{certBytes},
|
||||
PrivateKey: k,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type challTLSServer struct {
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func (c challTLSServer) Shutdown() error {
|
||||
return c.Server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func (c challTLSServer) ListenAndServe() error {
|
||||
// Since we set TLSConfig.GetCertificate, the certfile and keyFile arguments
|
||||
// are ignored and we leave them blank.
|
||||
return c.Server.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
func tlsALPNOneServer(address string, challSrv *ChallSrv) challengeServer {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: address,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
TLSConfig: &tls.Config{
|
||||
NextProtos: []string{ACMETLS1Protocol},
|
||||
GetCertificate: challSrv.ServeChallengeCertFunc(key),
|
||||
},
|
||||
}
|
||||
srv.SetKeepAlivesEnabled(false)
|
||||
return challTLSServer{srv}
|
||||
}
|
||||
Loading…
Reference in New Issue