diff --git a/acme/common.go b/acme/common.go index d3a1f9d..423c6fa 100644 --- a/acme/common.go +++ b/acme/common.go @@ -13,6 +13,7 @@ const ( StatusDeactivated = "deactivated" IdentifierDNS = "dns" + IdentifierIP = "ip" ChallengeHTTP01 = "http-01" ChallengeTLSALPN01 = "tls-alpn-01" diff --git a/ca/ca.go b/ca/ca.go index d973401..764a03d 100644 --- a/ca/ca.go +++ b/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 @@ -171,7 +174,8 @@ func (ca *CAImpl) newCertificate(domains []string, key crypto.PublicKey, account serial := makeSerial() template := &x509.Certificate{ - DNSNames: domains, + 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 diff --git a/core/types.go b/core/types.go index 0a5cbf3..a90c9c6 100644 --- a/core/types.go +++ b/core/types.go @@ -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 diff --git a/va/va.go b/va/va.go index f057509..364bbca 100644 --- a/va/va.go +++ b/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, ".") +} diff --git a/wfe/wfe.go b/wfe/wfe.go index bf113dd..d2327c4 100644 --- a/wfe/wfe.go +++ b/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