Implement reloadable JSON blacklist.

This eliminates the need the a database to store the hostname policy,
simplifying deployment. We keep the database for now, as part of our
deployability guidelines: we'll deploy, then switch config to the new style.

This also disables the obsolete whitelist checking code, but doesn't yet change
the function signature for policy.New(), to avoid bloating the pull request.
I'll fully remove the whitelist checking code in a future change when I also
remove the policy database code.
This commit is contained in:
Jacob Hoffman-Andrews 2016-03-06 18:45:20 -08:00
parent 0a0454f837
commit bc28bfe906
7 changed files with 116 additions and 126 deletions

View File

@ -15,6 +15,7 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/pkcs11key"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/ca"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
@ -73,13 +74,21 @@ func main() {
go cmd.DebugServer(c.CA.DebugAddr)
dbURL, err := c.PA.DBConfig.URL()
cmd.FailOnError(err, "Couldn't load DB URL")
paDbMap, err := sa.NewDbMap(dbURL)
cmd.FailOnError(err, "Couldn't connect to policy database")
var paDbMap *gorp.DbMap
if c.CA.HostnamePolicyFile == "" {
dbURL, err := c.PA.DBConfig.URL()
cmd.FailOnError(err, "Couldn't load DB URL")
paDbMap, err = sa.NewDbMap(dbURL)
cmd.FailOnError(err, "Couldn't connect to policy database")
}
pa, err := policy.New(paDbMap, c.PA.EnforcePolicyWhitelist, c.PA.Challenges)
cmd.FailOnError(err, "Couldn't create PA")
if c.CA.HostnamePolicyFile != "" {
err = pa.SetHostnamePolicyFile(c.RA.HostnamePolicyFile)
cmd.FailOnError(err, "Couldn't load hostname policy file")
}
priv, err := loadPrivateKey(c.CA.Key)
cmd.FailOnError(err, "Couldn't load private key")

View File

@ -10,6 +10,7 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/policy"
@ -31,13 +32,21 @@ func main() {
go cmd.DebugServer(c.RA.DebugAddr)
dbURL, err := c.PA.DBConfig.URL()
cmd.FailOnError(err, "Couldn't load DB URL")
paDbMap, err := sa.NewDbMap(dbURL)
cmd.FailOnError(err, "Couldn't connect to policy database")
var paDbMap *gorp.DbMap
if c.RA.HostnamePolicyFile == "" {
dbURL, err := c.PA.DBConfig.URL()
cmd.FailOnError(err, "Couldn't load DB URL")
paDbMap, err = sa.NewDbMap(dbURL)
cmd.FailOnError(err, "Couldn't connect to policy database")
}
pa, err := policy.New(paDbMap, c.PA.EnforcePolicyWhitelist, c.PA.Challenges)
cmd.FailOnError(err, "Couldn't create PA")
if c.RA.HostnamePolicyFile != "" {
err = pa.SetHostnamePolicyFile(c.RA.HostnamePolicyFile)
cmd.FailOnError(err, "Couldn't load hostname policy file")
}
rateLimitPolicies, err := cmd.LoadRateLimitPolicies(c.RA.RateLimitPoliciesFilename)
cmd.FailOnError(err, "Couldn't load rate limit policies file")

View File

@ -53,6 +53,7 @@ type Config struct {
RA struct {
ServiceConfig
HostnamePolicyConfig
RateLimitPoliciesFilename string
@ -308,6 +309,7 @@ func (a *AMQPConfig) ServerURL() (string, error) {
type CAConfig struct {
ServiceConfig
DBConfig
HostnamePolicyConfig
Profile string
RSAProfile string
@ -346,6 +348,12 @@ type PAConfig struct {
Challenges map[string]bool
}
// HostnamePolicyConfig specifies a file from which to load a policy regarding
// what hostnames to issue for.
type HostnamePolicyConfig struct {
HostnamePolicyFile string
}
// CheckChallenges checks whether the list of challenges in the PA config
// actually contains valid challenge names
func (pc PAConfig) CheckChallenges() error {

View File

@ -6,16 +6,22 @@
package policy
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"math/rand"
"net"
"regexp"
"strings"
"sync"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/net/publicsuffix"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/reloader"
)
// AuthorityImpl enforces CA policy decisions.
@ -23,26 +29,31 @@ type AuthorityImpl struct {
log *blog.AuditLogger
DB *AuthorityDatabaseImpl
EnforceWhitelist bool
blacklist map[string]bool
blacklistMu sync.RWMutex
enabledChallenges map[string]bool
pseudoRNG *rand.Rand
}
// New constructs a Policy Authority.
func New(dbMap *gorp.DbMap, enforceWhitelist bool, challengeTypes map[string]bool) (*AuthorityImpl, error) {
func New(dbMap *gorp.DbMap, _ bool, challengeTypes map[string]bool) (*AuthorityImpl, error) {
logger := blog.GetAuditLogger()
logger.Notice("Policy Authority Starting")
// Setup policy db
padb, err := NewAuthorityDatabaseImpl(dbMap)
if err != nil {
return nil, err
var padb *AuthorityDatabaseImpl
var err error
if dbMap != nil {
// Setup policy db
padb, err = NewAuthorityDatabaseImpl(dbMap)
if err != nil {
return nil, err
}
}
pa := AuthorityImpl{
log: logger,
DB: padb,
EnforceWhitelist: enforceWhitelist,
enabledChallenges: challengeTypes,
// We don't need real randomness for this.
pseudoRNG: rand.New(rand.NewSource(99)),
@ -51,6 +62,42 @@ func New(dbMap *gorp.DbMap, enforceWhitelist bool, challengeTypes map[string]boo
return &pa, nil
}
type blacklistJSON struct {
Blacklist []string
}
// SetHostnamePolicyFile will load the given policy file, returning error if it
// fails. It will also start a reloader in case the file changes.
func (pa *AuthorityImpl) SetHostnamePolicyFile(f string) error {
_, err := reloader.New(f, pa.loadHostnamePolicy)
return err
}
func (pa *AuthorityImpl) loadHostnamePolicy(b []byte, err error) error {
if err != nil {
pa.log.Err(fmt.Sprintf("loading hostname policy: %s", err))
}
hash := sha256.Sum256(b)
pa.log.Info(fmt.Sprintf("loading hostname policy, sha256: %s",
hex.EncodeToString(hash[:])))
var bl blacklistJSON
err = json.Unmarshal(b, &bl)
if err != nil {
return err
}
if len(bl.Blacklist) == 0 {
return fmt.Errorf("No entries in blacklist.")
}
nameMap := make(map[string]bool)
for _, v := range bl.Blacklist {
nameMap[v] = true
}
pa.blacklistMu.Lock()
pa.blacklist = nameMap
pa.blacklistMu.Unlock()
return nil
}
const (
maxLabels = 10
@ -127,7 +174,7 @@ var (
// where comparison is case-independent (normalized to lower case)
//
// If WillingToIssue returns an error, it will be of type MalformedRequestError.
func (pa AuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64) error {
func (pa *AuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64) error {
if id.Type != core.IdentifierDNS {
return errInvalidIdentifier
}
@ -188,28 +235,41 @@ func (pa AuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64) erro
return errICANNTLD
}
// Use the domain whitelist if the PA has been asked to. However, if the
// registration ID is from a whitelisted partner we're allowing to register
// any domain, they can get in, too.
enforceWhitelist := pa.EnforceWhitelist
if regID == whitelistedPartnerRegID {
enforceWhitelist = false
}
// Require no match against blacklist and if enforceWhitelist is true
// require domain to match a whitelist rule.
if err := pa.DB.CheckHostLists(domain, enforceWhitelist); err != nil {
// Require no match against blacklist
if err := pa.checkHostLists(domain); err != nil {
return err
}
return nil
}
func (pa *AuthorityImpl) checkHostLists(domain string) error {
if pa.DB != nil {
return pa.DB.CheckHostLists(domain, false)
}
pa.blacklistMu.RLock()
defer pa.blacklistMu.RUnlock()
if pa.blacklist == nil {
return fmt.Errorf("Hostname policy not yet loaded.")
}
labels := strings.Split(domain, ".")
for i := range labels {
joined := strings.Join(labels[i:], ".")
if pa.blacklist[joined] {
return errBlacklisted
}
}
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 AuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier, accountKey *jose.JsonWebKey) ([]core.Challenge, [][]int) {
func (pa *AuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier, accountKey *jose.JsonWebKey) ([]core.Challenge, [][]int) {
challenges := []core.Challenge{}
if pa.enabledChallenges[core.ChallengeTypeHTTP01] {

View File

@ -213,100 +213,3 @@ func TestChallengesFor(t *testing.T) {
test.AssertEquals(t, len(seenChalls), len(enabledChallenges))
test.AssertDeepEquals(t, expectedCombos, combinations)
}
func TestWillingToIssueWithWhitelist(t *testing.T) {
dbMap, cleanUp := paDBMap(t)
defer cleanUp()
pa, err := New(dbMap, true, nil)
test.AssertNotError(t, err, "Couldn't create policy implementation")
googID := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "www.google.com",
}
zomboID := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "www.zombo.com",
}
type listTestCase struct {
regID int64
id core.AcmeIdentifier
err error
}
pa.DB.LoadRules(RuleSet{
Whitelist: []WhitelistRule{
{Host: "www.zombo.com"},
},
})
// Note that www.google.com is not in the blacklist for this test. We no
// longer have a hardcoded blacklist.
testCases := []listTestCase{
{100, googID, errNotWhitelisted},
{whitelistedPartnerRegID, googID, nil},
{100, zomboID, nil},
{whitelistedPartnerRegID, zomboID, nil},
}
for _, tc := range testCases {
err := pa.WillingToIssue(tc.id, tc.regID)
if err != tc.err {
t.Errorf("%#v, %d: want %#v, got %#v", tc.id.Value, tc.regID, tc.err, err)
}
}
pa.DB.LoadRules(RuleSet{
Blacklist: []BlacklistRule{
{Host: "www.google.com"},
},
Whitelist: []WhitelistRule{
{Host: "www.zombo.com"},
},
})
exampleID := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "www.example.com",
}
testCases = []listTestCase{
// This errNotWhitelisted is surprising and accidental from the ordering
// of the whitelist and blacklist check. The whitelist will be gone soon
// enough.
{100, googID, errNotWhitelisted},
{whitelistedPartnerRegID, googID, errBlacklisted},
{100, zomboID, nil},
{whitelistedPartnerRegID, zomboID, nil},
{100, exampleID, errNotWhitelisted},
{whitelistedPartnerRegID, exampleID, nil},
}
for _, tc := range testCases {
err := pa.WillingToIssue(tc.id, tc.regID)
if err != tc.err {
t.Errorf("%#v, %d: want %#v, got %#v", tc.id.Value, tc.regID, tc.err, err)
}
}
pa.DB.LoadRules(RuleSet{
Blacklist: []BlacklistRule{
{Host: "www.google.com"},
},
Whitelist: []WhitelistRule{
{Host: "www.zombo.com"},
{Host: "www.google.com"},
},
})
testCases = []listTestCase{
{100, googID, errBlacklisted},
{whitelistedPartnerRegID, googID, errBlacklisted},
{100, zomboID, nil},
{whitelistedPartnerRegID, zomboID, nil},
{100, exampleID, errNotWhitelisted},
{whitelistedPartnerRegID, exampleID, nil},
}
for _, tc := range testCases {
err := pa.WillingToIssue(tc.id, tc.regID)
if err != tc.err {
t.Errorf("%#v, %d: want %#v, got %#v", tc.id.Value, tc.regID, tc.err, err)
}
}
}

View File

@ -47,6 +47,7 @@
"maxNames": 1000,
"doNotForceCN": true,
"enableMustStaple": true,
"hostnamePolicyFile": "test/hostname-policy.json",
"cfssl": {
"signing": {
"profiles": {
@ -150,7 +151,6 @@
},
"pa": {
"dbConnect": "mysql+tcp://policy@localhost:3306/boulder_policy_integration?readTimeout=800ms&writeTimeout=800ms",
"challenges": {
"http-01": true,
"tls-sni-01": true,
@ -164,6 +164,7 @@
"maxContactsPerRegistration": 100,
"dnsTries": 3,
"debugAddr": "localhost:8002",
"hostnamePolicyFile": "test/hostname-policy.json",
"amqp": {
"serverURLFile": "test/secrets/amqp_url",
"insecure": true,