178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
// Copyright 2014 ISRG. All rights reserved
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package policy
|
|
|
|
import (
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
)
|
|
|
|
// PolicyAuthorityImpl enforces CA policy decisions.
|
|
type PolicyAuthorityImpl struct {
|
|
log *blog.AuditLogger
|
|
|
|
PublicSuffixList map[string]bool // A copy of the DNS root zone
|
|
Blacklist map[string]bool // A blacklist of denied names
|
|
}
|
|
|
|
// NewPolicyAuthorityImpl constructs a Policy Authority.
|
|
func NewPolicyAuthorityImpl() *PolicyAuthorityImpl {
|
|
logger := blog.GetAuditLogger()
|
|
logger.Notice("Policy Authority Starting")
|
|
|
|
pa := PolicyAuthorityImpl{log: logger}
|
|
|
|
// TODO: Add configurability
|
|
pa.PublicSuffixList = PublicSuffixList
|
|
pa.Blacklist = blacklist
|
|
|
|
return &pa
|
|
}
|
|
|
|
const maxLabels = 10
|
|
|
|
var dnsLabelRegexp = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$")
|
|
var punycodeRegexp = regexp.MustCompile("^xn--")
|
|
|
|
func isDNSCharacter(ch byte) bool {
|
|
return ('a' <= ch && ch <= 'z') ||
|
|
('A' <= ch && ch <= 'Z') ||
|
|
('0' <= ch && ch <= '9') ||
|
|
ch == '.' || ch == '-'
|
|
}
|
|
|
|
// Test whether the domain name indicated by the label set is a label-wise
|
|
// suffix match for the provided suffix set. If the `properSuffix` flag is
|
|
// set, then the name is required to not be in the suffix set (i.e., it must
|
|
// have at least one label beyond any suffix in the set).
|
|
func suffixMatch(labels []string, suffixSet map[string]bool, properSuffix bool) bool {
|
|
for i := range labels {
|
|
if domain := strings.Join(labels[i:], "."); suffixSet[domain] {
|
|
// If we match on the whole domain, gate on properSuffix
|
|
return !properSuffix || (i > 0)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// InvalidIdentifierError indicates that we didn't understand the IdentifierType
|
|
// provided.
|
|
type InvalidIdentifierError struct{}
|
|
|
|
// SyntaxError indicates that the user input was not well formatted.
|
|
type SyntaxError struct{}
|
|
|
|
// NonPublicError indicates that one or more identifiers were not on the public
|
|
// Internet.
|
|
type NonPublicError struct{}
|
|
|
|
// BlacklistedError indicates we have blacklisted one or more of these identifiers.
|
|
type BlacklistedError struct{}
|
|
|
|
func (e InvalidIdentifierError) Error() string { return "Invalid identifier type" }
|
|
func (e SyntaxError) Error() string { return "Syntax error" }
|
|
func (e NonPublicError) Error() string { return "Name does not end in a public suffix" }
|
|
func (e BlacklistedError) Error() string { return "Name is blacklisted" }
|
|
|
|
// WillingToIssue determines whether the CA is willing to issue for the provided
|
|
// identifier.
|
|
//
|
|
// We place several criteria on identifiers we are willing to issue for:
|
|
//
|
|
// * MUST self-identify as DNS identifiers
|
|
// * MUST contain only bytes in the DNS hostname character set
|
|
// * MUST NOT have more than maxLabels labels
|
|
// * MUST follow the DNS hostname syntax rules in RFC 1035 and RFC 2181
|
|
// In particular:
|
|
// * MUST NOT contain underscores
|
|
// * MUST NOT contain IDN labels (xn--)
|
|
// * MUST NOT match the syntax of an IP address
|
|
// * MUST end in a public suffix
|
|
// * MUST have at least one label in addition to the public suffix
|
|
// * MUST NOT be a label-wise suffix match for a name on the black list,
|
|
// where comparison is case-independent (normalized to lower case)
|
|
//
|
|
// XXX: Is there any need for this method to be constant-time? We're
|
|
// going to refuse to issue anyway, but timing could leak whether
|
|
// names are on the blacklist.
|
|
//
|
|
// XXX: We should probably fold everything to lower-case somehow.
|
|
func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier) error {
|
|
if id.Type != core.IdentifierDNS {
|
|
return InvalidIdentifierError{}
|
|
}
|
|
domain := id.Value
|
|
|
|
for _, ch := range []byte(domain) {
|
|
if !isDNSCharacter(ch) {
|
|
return SyntaxError{}
|
|
}
|
|
}
|
|
|
|
domain = strings.ToLower(domain)
|
|
if len(domain) > 255 {
|
|
return SyntaxError{}
|
|
}
|
|
|
|
if ip := net.ParseIP(domain); ip != nil {
|
|
return SyntaxError{}
|
|
}
|
|
|
|
labels := strings.Split(domain, ".")
|
|
if len(labels) > maxLabels || len(labels) < 2 {
|
|
return SyntaxError{}
|
|
}
|
|
for _, label := range labels {
|
|
// DNS defines max label length as 63 characters. Some implementations allow
|
|
// more, but we will be conservative.
|
|
if len(label) < 1 || len(label) > 63 {
|
|
return SyntaxError{}
|
|
}
|
|
|
|
if !dnsLabelRegexp.MatchString(label) {
|
|
return SyntaxError{}
|
|
}
|
|
|
|
if punycodeRegexp.MatchString(label) {
|
|
return SyntaxError{}
|
|
}
|
|
}
|
|
|
|
// Require match to PSL, plus at least one label
|
|
if !suffixMatch(labels, pa.PublicSuffixList, true) {
|
|
return NonPublicError{}
|
|
}
|
|
|
|
// Require no match against blacklist
|
|
if suffixMatch(labels, pa.Blacklist, false) {
|
|
return BlacklistedError{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ChallengesFor makes a decision of what challenges, and combinations, are
|
|
// acceptable for the given identifier.
|
|
//
|
|
// Note: Current implementation is static, but future versions may not be.
|
|
func (pa PolicyAuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier) (challenges []core.Challenge, combinations [][]int) {
|
|
challenges = []core.Challenge{
|
|
core.SimpleHTTPChallenge(),
|
|
core.DvsniChallenge(),
|
|
core.DNSChallenge(),
|
|
}
|
|
combinations = [][]int{
|
|
[]int{0},
|
|
[]int{1},
|
|
[]int{2},
|
|
}
|
|
return
|
|
}
|