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:
parent
0ecf7e0534
commit
bc4da68d49
|
|
@ -13,6 +13,7 @@ const (
|
|||
StatusDeactivated = "deactivated"
|
||||
|
||||
IdentifierDNS = "dns"
|
||||
IdentifierIP = "ip"
|
||||
|
||||
ChallengeHTTP01 = "http-01"
|
||||
ChallengeTLSALPN01 = "tls-alpn-01"
|
||||
|
|
|
|||
10
ca/ca.go
10
ca/ca.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
56
va/va.go
56
va/va.go
|
|
@ -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, ".")
|
||||
}
|
||||
|
|
|
|||
138
wfe/wfe.go
138
wfe/wfe.go
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue