Support draft-acme-ip and IP address identifiers (#221)

Implements https://datatracker.ietf.org/doc/draft-ietf-acme-ip/ support in Pebble.
This commit is contained in:
orangepizza 2019-06-04 02:41:16 +09:00 committed by Daniel McCarney
parent 0ecf7e0534
commit bc4da68d49
5 changed files with 165 additions and 44 deletions

View File

@ -13,6 +13,7 @@ const (
StatusDeactivated = "deactivated"
IdentifierDNS = "dns"
IdentifierIP = "ip"
ChallengeHTTP01 = "http-01"
ChallengeTLSALPN01 = "tls-alpn-01"

View File

@ -11,6 +11,7 @@ import (
"log"
"math"
"math/big"
"net"
"time"
"github.com/letsencrypt/pebble/acme"
@ -156,12 +157,14 @@ func (ca *CAImpl) newIntermediateIssuer() error {
return nil
}
func (ca *CAImpl) newCertificate(domains []string, key crypto.PublicKey, accountID string) (*core.Certificate, error) {
func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.PublicKey, accountID string) (*core.Certificate, error) {
var cn string
if len(domains) > 0 {
cn = domains[0]
} else if len(ips) > 0 {
cn = ips[0].String()
} else {
return nil, fmt.Errorf("must specify at least one domain name")
return nil, fmt.Errorf("must specify at least one domain name or IP address")
}
issuer := ca.intermediate
@ -172,6 +175,7 @@ func (ca *CAImpl) newCertificate(domains []string, key crypto.PublicKey, account
serial := makeSerial()
template := &x509.Certificate{
DNSNames: domains,
IPAddresses: ips,
Subject: pkix.Name{
CommonName: cn,
},
@ -250,7 +254,7 @@ func (ca *CAImpl) CompleteOrder(order *core.Order) {
// issue a certificate for the csr
csr := order.ParsedCSR
cert, err := ca.newCertificate(csr.DNSNames, csr.PublicKey, order.AccountID)
cert, err := ca.newCertificate(csr.DNSNames, csr.IPAddresses, csr.PublicKey, order.AccountID)
if err != nil {
ca.log.Printf("Error: unable to issue order: %s", err.Error())
return

View File

@ -73,7 +73,7 @@ func (o *Order) GetStatus() (string, error) {
return acme.StatusPending, nil
}
fullyAuthorized := len(o.Names) == authzStatuses[acme.StatusValid]
fullyAuthorized := len(o.Identifiers) == authzStatuses[acme.StatusValid]
// If the order isn't fully authorized we've encountered an internal error:
// Above we checked for any invalid or pending authzs and should have returned

View File

@ -87,7 +87,7 @@ func certNames(cert *x509.Certificate) string {
}
type vaTask struct {
Identifier string
Identifier acme.Identifier
Challenge *core.Challenge
Account *core.Account
}
@ -144,7 +144,7 @@ func New(
return va
}
func (va VAImpl) ValidateChallenge(ident string, chal *core.Challenge, acct *core.Account) {
func (va VAImpl) ValidateChallenge(ident acme.Identifier, chal *core.Challenge, acct *core.Account) {
task := &vaTask{
Identifier: ident,
Challenge: chal,
@ -272,7 +272,7 @@ func (va VAImpl) performValidation(task *vaTask, results chan<- *core.Validation
// type. For example comparison, a real DNS-01 validation would set
// the URL to the `_acme-challenge` subdomain.
results <- &core.ValidationRecord{
URL: task.Identifier,
URL: task.Identifier.Value,
ValidatedAt: time.Now(),
}
return
@ -333,15 +333,21 @@ func (va VAImpl) validateDNS01(task *vaTask) *core.ValidationRecord {
func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord {
portString := strconv.Itoa(va.tlsPort)
hostPort := net.JoinHostPort(task.Identifier, portString)
hostPort := net.JoinHostPort(task.Identifier.Value, portString)
var serverNameIdentifier string
switch task.Identifier.Type {
case acme.IdentifierDNS:
serverNameIdentifier = task.Identifier.Value
case acme.IdentifierIP:
serverNameIdentifier = reverseaddr(task.Identifier.Value)
}
result := &core.ValidationRecord{
URL: hostPort,
ValidatedAt: time.Now(),
}
cs, problem := va.fetchConnectionState(hostPort, &tls.Config{
ServerName: task.Identifier,
ServerName: serverNameIdentifier,
NextProtos: []string{acme.ACMETLS1Protocol},
InsecureSkipVerify: true,
})
@ -367,7 +373,16 @@ func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord {
leafCert := certs[0]
// Verify SNI - certificate returned must be issued only for the domain we are verifying.
if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], task.Identifier) {
var namematch bool
switch task.Identifier.Type {
case acme.IdentifierDNS:
namematch = len(leafCert.DNSNames) == 1 && strings.EqualFold(leafCert.DNSNames[0], task.Identifier.Value)
case acme.IdentifierIP:
namematch = len(leafCert.IPAddresses) == 1 && leafCert.IPAddresses[0].Equal(net.ParseIP(task.Identifier.Value))
default:
namematch = false
}
if !namematch {
names := certNames(leafCert)
errText := fmt.Sprintf(
"Incorrect validation certificate for %s challenge. "+
@ -430,7 +445,7 @@ func (va VAImpl) fetchConnectionState(hostPort string, config *tls.Config) (*tls
}
func (va VAImpl) validateHTTP01(task *vaTask) *core.ValidationRecord {
body, url, err := va.fetchHTTP(task.Identifier, task.Challenge.Token)
body, url, err := va.fetchHTTP(task.Identifier.Value, task.Challenge.Token)
result := &core.ValidationRecord{
URL: url,
@ -458,10 +473,11 @@ func (va VAImpl) validateHTTP01(task *vaTask) *core.ValidationRecord {
// purpose HTTP function
func (va VAImpl) fetchHTTP(identifier string, token string) ([]byte, string, *acme.ProblemDetails) {
path := fmt.Sprintf("%s%s", acme.HTTP01BaseURL, token)
portString := strconv.Itoa(va.httpPort)
url := &url.URL{
Scheme: "http",
Host: fmt.Sprintf("%s:%d", identifier, va.httpPort),
Host: net.JoinHostPort(identifier, portString),
Path: path,
}
@ -517,3 +533,25 @@ func (va VAImpl) fetchHTTP(identifier string, token string) ([]byte, string, *ac
return body, url.String(), nil
}
// reverseaddr function is borrowed from net/dnsclient.go[0] and the Go std library.
// [0]: https://golang.org/src/net/dnsclient.go
func reverseaddr(addr string) string {
ip := net.ParseIP(addr)
if ip == nil {
return ""
}
// Apperently IP type in net package saves all ip in ipv6 formant, from biggest byte to smallest. we need last 4 bytes, so ip[15] to ip[12]
if ip.To4() != nil {
return fmt.Sprintf("%d.%d.%d.%d.in-addr.arpa.", ip[15], ip[14], ip[13], ip[12])
}
// Must be IPv6
buf := make([]string, 0, len(ip)+1)
// Add it, in reverse, to the buffer
for i := len(ip) - 1; i >= 0; i-- {
buf = append(buf, fmt.Sprintf("%x.%x", ip[i]&0x0F, ip[i]>>4))
}
// Append "ip6.arpa." and return (buf already has the final '.') see RFC3152 for how this address is constructed.
buf = append(buf, "ip6.arpa.")
return strings.Join(buf, ".")
}

View File

@ -1,6 +1,7 @@
package wfe
import (
"bytes"
"context"
"crypto"
"crypto/x509"
@ -1013,11 +1014,20 @@ func (wfe *WebFrontEndImpl) verifyOrder(order *core.Order) *acme.ProblemDetails
if len(idents) == 0 {
return acme.MalformedProblem("Order did not specify any identifiers")
}
// Check that all of the identifiers in the new-order are DNS type
// Check that all of the identifiers in the new-order are DNS or IPaddress type
// Validity check of ipaddresses are done here.
for _, ident := range idents {
if ident.Type == acme.IdentifierIP {
if net.ParseIP(ident.Value) == nil {
return acme.MalformedProblem(fmt.Sprintf(
"Order included malformed IP type identifier value: %q\n",
ident.Value))
}
continue
}
if ident.Type != acme.IdentifierDNS {
return acme.MalformedProblem(fmt.Sprintf(
"Order included non-DNS type identifier: type %q, value %q",
"Order included unsupported type identifier: type %q, value %q",
ident.Type, ident.Value))
}
@ -1083,12 +1093,12 @@ func (wfe *WebFrontEndImpl) makeAuthorizations(order *core.Order, request *http.
// Lock the order for reading
order.RLock()
// Create one authz for each name in the order's parsed CSR
for _, name := range order.Names {
for _, name := range order.Identifiers {
now := time.Now().UTC()
expires := now.Add(pendingAuthzExpire)
ident := acme.Identifier{
Type: acme.IdentifierDNS,
Value: name,
Type: name.Type,
Value: name.Value,
}
authz := &core.Authorization{
ID: newToken(),
@ -1166,8 +1176,14 @@ func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *h
}
chals = []*core.Challenge{chal}
} else {
// Non-wildcard authorizations get all of the enabled challenge types
enabledChallenges := []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01}
// IP addresses get HTTP-01 and TLS-ALPN challenges
var enabledChallenges []string
if authz.Identifier.Value == acme.IdentifierIP {
enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01}
} else {
// Non-wildcard, non-IP identifier authorizations get all of the enabled challenge types
enabledChallenges = []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01}
}
for _, chalType := range enabledChallenges {
chal, err := wfe.makeChallenge(chalType, authz, request)
if err != nil {
@ -1211,6 +1227,28 @@ func (wfe *WebFrontEndImpl) NewOrder(
return
}
var orderDNSs []string
var orderIPs []net.IP
for _, ident := range newOrder.Identifiers {
switch ident.Type {
case acme.IdentifierDNS:
orderDNSs = append(orderDNSs, ident.Value)
case acme.IdentifierIP:
orderIPs = append(orderIPs, net.ParseIP(ident.Value))
default:
wfe.sendError(acme.MalformedProblem(
fmt.Sprintf("Order includes unknown identifier type %s", ident.Type)), response)
}
}
orderDNSs = uniqueLowerNames(orderDNSs)
orderIPs = uniqueIPs(orderIPs)
var uniquenames []acme.Identifier
for _, name := range orderDNSs {
uniquenames = append(uniquenames, acme.Identifier{Value: name, Type: acme.IdentifierDNS})
}
for _, ip := range orderIPs {
uniquenames = append(uniquenames, acme.Identifier{Value: ip.String(), Type: acme.IdentifierIP})
}
expires := time.Now().AddDate(0, 0, 1)
order := &core.Order{
ID: newToken(),
@ -1220,7 +1258,7 @@ func (wfe *WebFrontEndImpl) NewOrder(
Expires: expires.UTC().Format(time.RFC3339),
// Only the Identifiers, NotBefore and NotAfter from the submitted order
// are carried forward
Identifiers: newOrder.Identifiers,
Identifiers: uniquenames,
NotBefore: newOrder.NotBefore,
NotAfter: newOrder.NotAfter,
},
@ -1233,15 +1271,6 @@ func (wfe *WebFrontEndImpl) NewOrder(
return
}
// Collect all of the DNS identifier values up into a []string
var orderNames []string
for _, ident := range order.Identifiers {
orderNames = append(orderNames, ident.Value)
}
// Store the unique lower version of the names on the order object
order.Names = uniqueLowerNames(orderNames)
// Create the authorizations for the order
err = wfe.makeAuthorizations(order, request)
if err != nil {
@ -1397,7 +1426,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(
orderAccountID := existingOrder.AccountID
orderStatus := existingOrder.Status
orderExpires := existingOrder.ExpiresDate
orderNames := existingOrder.Names
orderIdentifiers := existingOrder.Identifiers
// And then immediately unlock it again - we don't defer() here because
// `maybeIssue` will also acquire a read lock and we call that before
// returning
@ -1450,22 +1479,55 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(
return
}
// split order identifiers per types
var orderDNSs []string
var orderIPs []net.IP
for _, ident := range orderIdentifiers {
switch ident.Type {
case acme.IdentifierDNS:
orderDNSs = append(orderDNSs, ident.Value)
case acme.IdentifierIP:
orderIPs = append(orderIPs, net.ParseIP(ident.Value))
default:
wfe.sendError(acme.MalformedProblem(
fmt.Sprintf("Order includes unknown identifier type %s", ident.Type)), response)
}
}
// looks like saving order to db doesn't preserve order of Identifiers, so sort them again.
orderDNSs = uniqueLowerNames(orderDNSs)
orderIPs = uniqueIPs(orderIPs)
// sort and deduplicate CSR SANs
csrDNSs := uniqueLowerNames(parsedCSR.DNSNames)
csrIPs := uniqueIPs(parsedCSR.IPAddresses)
// Check that the CSR has the same number of names as the initial order contained
csrNames := uniqueLowerNames(parsedCSR.DNSNames)
if len(csrNames) != len(orderNames) {
if len(csrDNSs) != len(orderDNSs) {
wfe.sendError(acme.UnauthorizedProblem(
"Order includes different number of names than CSR specifies"), response)
"Order includes different number of DNSnames identifiers than CSR specifies"), response)
return
}
if len(csrIPs) != len(orderIPs) {
wfe.sendError(acme.UnauthorizedProblem(
"Order includes different number of IP address identifiers than CSR specifies"), response)
return
}
// Check that the CSR's names match the order names exactly
for i, name := range orderNames {
if name != csrNames[i] {
for i, name := range orderDNSs {
if name != csrDNSs[i] {
wfe.sendError(acme.UnauthorizedProblem(
fmt.Sprintf("CSR is missing Order domain %q", name)), response)
return
}
}
for i, IP := range orderIPs {
if !csrIPs[i].Equal(IP) {
wfe.sendError(acme.UnauthorizedProblem(
fmt.Sprintf("CSR is missing Order IP %q", IP)), response)
return
}
}
// Lock and update the order with the parsed CSR and the began processing
// state.
@ -1718,12 +1780,11 @@ func (wfe *WebFrontEndImpl) validateAuthzForChallenge(authz *core.Authorization)
defer authz.RUnlock()
ident := authz.Identifier
if ident.Type != acme.IdentifierDNS {
if ident.Type != acme.IdentifierDNS && ident.Type != acme.IdentifierIP {
return nil, acme.MalformedProblem(
fmt.Sprintf("Authorization identifier was type %s, only %s is supported",
ident.Type, acme.IdentifierDNS))
fmt.Sprintf("Authorization identifier was type %s, only %s and %s are supported",
ident.Type, acme.IdentifierDNS, acme.IdentifierIP))
}
now := time.Now()
if now.After(authz.ExpiresDate) {
return nil, acme.MalformedProblem(
@ -1825,14 +1886,14 @@ func (wfe *WebFrontEndImpl) updateChallenge(
// Lock the authorization to get the identifier value
authz.RLock()
ident := authz.Identifier.Value
ident := authz.Identifier
authz.RUnlock()
// If the identifier value is for a wildcard domain then strip the wildcard
// prefix before dispatching the validation to ensure the base domain is
// validated.
if strings.HasPrefix(ident, "*.") {
ident = strings.TrimPrefix(ident, "*.")
if strings.HasPrefix(ident.Value, "*.") {
ident.Value = strings.TrimPrefix(ident.Value, "*.")
}
// Submit a validation job to the VA, this will be processed asynchronously
@ -1926,6 +1987,23 @@ func uniqueLowerNames(names []string) []string {
return unique
}
// uniqueIPs returns the set of all unique IP addresses in the input.
// The returned IP addresses will be sorted in ascending order in text form.
func uniqueIPs(IPs []net.IP) []net.IP {
uniqMap := make(map[string]net.IP)
for _, ip := range IPs {
uniqMap[ip.String()] = ip
}
results := make([]net.IP, len(uniqMap))
for _, v := range uniqMap {
results = append(results, v)
}
sort.Slice(results, func(i, j int) bool {
return bytes.Compare(results[i], results[j]) < 0
})
return results
}
// RevokeCert revokes an ACME certificate.
// It currently only implements one method of ACME revocation:
// Signing the revocation request by signing it with the certificate