Merge branch 'master' into issued-names-limit-2

Conflicts:
	mocks/mocks.go
	rpc/rpc-wrappers.go
	sa/storage-authority.go
This commit is contained in:
Jacob Hoffman-Andrews 2015-10-07 16:58:05 -07:00
commit acdb1fa91b
43 changed files with 1706 additions and 594 deletions

View File

@ -203,7 +203,7 @@ func TestRevoke(t *testing.T) {
test.AssertNotError(t, err, "Failed to create CA")
ca.PA = ctx.pa
ca.SA = ctx.sa
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
csr, _ := x509.ParseCertificateRequest(CNandSANCSR)
certObj, err := ca.IssueCertificate(*csr, ctx.reg.ID)
@ -242,7 +242,7 @@ func TestIssueCertificate(t *testing.T) {
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
test.AssertNotError(t, err, "Failed to create CA")
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa
@ -319,7 +319,7 @@ func TestRejectNoName(t *testing.T) {
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
test.AssertNotError(t, err, "Failed to create CA")
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa
@ -336,7 +336,7 @@ func TestRejectTooManyNames(t *testing.T) {
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
test.AssertNotError(t, err, "Failed to create CA")
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa
@ -353,7 +353,7 @@ func TestDeduplication(t *testing.T) {
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
test.AssertNotError(t, err, "Failed to create CA")
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa
@ -377,7 +377,7 @@ func TestRejectValidityTooLong(t *testing.T) {
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
test.AssertNotError(t, err, "Failed to create CA")
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa
@ -394,7 +394,7 @@ func TestShortKey(t *testing.T) {
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa
@ -410,7 +410,7 @@ func TestRejectBadAlgorithm(t *testing.T) {
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile)
ca.Publisher = &mocks.MockPublisher{}
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa

View File

@ -130,6 +130,21 @@ func revokeByReg(regID int64, reasonCode core.RevocationCode, deny bool, rac rpc
return
}
// This abstraction is needed so that we can use sort.Sort below
type revocationCodes []core.RevocationCode
func (rc revocationCodes) Len() int {
return len(rc)
}
func (rc revocationCodes) Less(i, j int) bool {
return rc[i] < rc[j]
}
func (rc revocationCodes) Swap(i, j int) {
rc[i], rc[j] = rc[j], rc[i]
}
func main() {
app := cli.NewApp()
app.Name = "admin-revoker"
@ -219,7 +234,7 @@ func main() {
Name: "list-reasons",
Usage: "List all revocation reason codes",
Action: func(c *cli.Context) {
var codes core.RevocationCodes
var codes revocationCodes
for k := range core.RevocationReasons {
codes = append(codes, k)
}

View File

@ -39,18 +39,18 @@ func main() {
go cmd.ProfileCmd("VA", stats)
pc := &va.PortConfig{
SimpleHTTPPort: 80,
SimpleHTTPSPort: 443,
DVSNIPort: 443,
HTTPPort: 80,
HTTPSPort: 443,
TLSPort: 443,
}
if c.VA.PortConfig.SimpleHTTPPort != 0 {
pc.SimpleHTTPPort = c.VA.PortConfig.SimpleHTTPPort
if c.VA.PortConfig.HTTPPort != 0 {
pc.HTTPPort = c.VA.PortConfig.HTTPPort
}
if c.VA.PortConfig.SimpleHTTPSPort != 0 {
pc.SimpleHTTPSPort = c.VA.PortConfig.SimpleHTTPSPort
if c.VA.PortConfig.HTTPSPort != 0 {
pc.HTTPSPort = c.VA.PortConfig.HTTPSPort
}
if c.VA.PortConfig.DVSNIPort != 0 {
pc.DVSNIPort = c.VA.PortConfig.DVSNIPort
if c.VA.PortConfig.TLSPort != 0 {
pc.TLSPort = c.VA.PortConfig.TLSPort
}
vai := va.NewValidationAuthorityImpl(pc, stats, clock.Default())
dnsTimeout, err := time.ParseDuration(c.Common.DNSTimeout)

View File

@ -145,7 +145,7 @@ func TestCheckCert(t *testing.T) {
}
delete(problemsMap, p)
}
for k, _ := range problemsMap {
for k := range problemsMap {
t.Errorf("Found unexpected problem '%s'.", k)
}

View File

@ -56,11 +56,11 @@ func (m *mockMail) SendMail(to []string, msg string) (err error) {
}
type fakeRegStore struct {
RegById map[int64]core.Registration
RegByID map[int64]core.Registration
}
func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) {
r, ok := f.RegById[id]
r, ok := f.RegByID[id]
if !ok {
msg := fmt.Sprintf("no such registration %d", id)
return r, core.NoSuchRegistrationError(msg)
@ -69,7 +69,7 @@ func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) {
}
func newFakeRegStore() fakeRegStore {
return fakeRegStore{RegById: make(map[int64]core.Registration)}
return fakeRegStore{RegByID: make(map[int64]core.Registration)}
}
const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`

View File

@ -9,6 +9,7 @@ import (
"bytes"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"net/http"
"net/url"
@ -66,14 +67,13 @@ func NewSourceFromDatabase(dbMap *gorp.DbMap, caKeyHash []byte) (src *DBSource,
}
// Response is called by the HTTP server to handle a new OCSP request.
func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool) {
func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) {
log := blog.GetAuditLogger()
// Check that this request is for the proper CA
if bytes.Compare(req.IssuerKeyHash, src.caKeyHash) != 0 {
log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash)))
present = false
return
return nil, false
}
serialString := core.SerialToString(req.SerialNumber)
@ -86,37 +86,33 @@ func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool)
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;",
map[string]interface{}{"serial": serialString})
if err != nil {
present = false
return
return nil, false
}
log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString))
response = ocspResponse.Response
present = true
return
return ocspResponse.Response, true
}
func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (cfocsp.Source, error) {
var noSource cfocsp.Source
func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (*DBSource, error) {
// Configure DB
dbMap, err := sa.NewDbMap(dbConnect)
if err != nil {
return noSource, fmt.Errorf("Could not connect to database: %s", err)
return nil, fmt.Errorf("Could not connect to database: %s", err)
}
sa.SetSQLDebug(dbMap, sqlDebug)
// Load the CA's key so we can store its SubjectKey in the DB
caCertDER, err := cmd.LoadCert(issuerCert)
if err != nil {
return noSource, fmt.Errorf("Could not read issuer cert %s: %s", issuerCert, err)
return nil, fmt.Errorf("Could not read issuer cert %s: %s", issuerCert, err)
}
caCert, err := x509.ParseCertificate(caCertDER)
if err != nil {
return noSource, fmt.Errorf("Could not parse issuer cert %s: %s", issuerCert, err)
return nil, fmt.Errorf("Could not parse issuer cert %s: %s", issuerCert, err)
}
if len(caCert.SubjectKeyId) == 0 {
return noSource, fmt.Errorf("Empty subjectKeyID")
return nil, fmt.Errorf("Empty subjectKeyID")
}
// Construct source from DB
@ -161,6 +157,8 @@ func main() {
}
source, err = cfocsp.NewSourceFromFile(filename)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path))
} else {
cmd.FailOnError(errors.New(`"source" parameter not found in JSON config`), "unable to start ocsp-responder")
}
stopTimeout, err := time.ParseDuration(c.OCSPResponder.ShutdownStopTimeout)

View File

@ -1,16 +1,23 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
cfocsp "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/golang.org/x/crypto/ocsp"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/test"
)
func TestCacheControl(t *testing.T) {
src := make(ocsp.InMemorySource)
src := make(cfocsp.InMemorySource)
h := handler(src, 10*time.Second)
w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "/", nil)
@ -24,3 +31,80 @@ func TestCacheControl(t *testing.T) {
t.Errorf("Cache-Control value: want %#v, got %#v", expected, actual)
}
}
var (
req = mustRead("./testdata/ocsp.req")
resp = mustRead("./testdata/ocsp.resp")
)
func TestHandler(t *testing.T) {
ocspReq, err := ocsp.ParseRequest(req)
if err != nil {
t.Fatalf("ocsp.ParseRequest: %s", err)
}
src := make(cfocsp.InMemorySource)
src[ocspReq.SerialNumber.String()] = resp
h := handler(src, 10*time.Second)
w := httptest.NewRecorder()
r, err := http.NewRequest("POST", "/", bytes.NewReader(req))
if err != nil {
t.Fatal(err)
}
h.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Code: want %d, got %d", http.StatusOK, w.Code)
}
if !bytes.Equal(w.Body.Bytes(), resp) {
t.Errorf("Mismatched body: want %#v, got %#v", resp, w.Body.Bytes())
}
}
func TestDBHandler(t *testing.T) {
src, err := makeDBSource("mysql+tcp://boulder@localhost:3306/boulder_sa_test", "./testdata/test-ca.der.pem", false)
if err != nil {
t.Fatalf("makeDBSource: %s", err)
}
defer test.ResetTestDatabase(t, src.dbMap.Db)
ocspResp, err := ocsp.ParseResponse(resp, nil)
if err != nil {
t.Fatalf("ocsp.ParseResponse: %s", err)
}
dbOCSP := &core.OCSPResponse{
Serial: core.SerialToString(ocspResp.SerialNumber),
CreatedAt: time.Now(),
Response: resp,
}
err = src.dbMap.Insert(dbOCSP)
if err != nil {
t.Fatalf("unable to insert response: %s", err)
}
h := handler(src, 10*time.Second)
w := httptest.NewRecorder()
r, err := http.NewRequest("POST", "/", bytes.NewReader(req))
if err != nil {
t.Fatal(err)
}
h.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Code: want %d, got %d", http.StatusOK, w.Code)
}
if !bytes.Equal(w.Body.Bytes(), resp) {
t.Errorf("Mismatched body: want %#v, got %#v", resp, w.Body.Bytes())
}
}
func mustRead(path string) []byte {
f, err := os.Open(path)
if err != nil {
panic(fmt.Sprintf("open %#v: %s", path, err))
}
b, err := ioutil.ReadAll(f)
if err != nil {
panic(fmt.Sprintf("read all %#v: %s", path, err))
}
return b
}

BIN
cmd/ocsp-responder/testdata/ocsp.req vendored Normal file

Binary file not shown.

BIN
cmd/ocsp-responder/testdata/ocsp.resp vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV
BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw
NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i
8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8
tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj
7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8
BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD
HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj
UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7
eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB
vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl
zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo
vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L
oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW
rFo4Uv1EnkKJm3vJFe50eJGhEKlx
-----END CERTIFICATE-----

28
cmd/ocsp-responder/testdata/test-ca.key vendored Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDCCkd5mgXFErJ3
F2M0E9dw+Ta/md5i8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9
r9tSQcL8VM6WUOM8tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHS
zUYtNKNeFI6Glamj7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p5
8QkP4LHLShVLXDa8BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aP
kE/cefmP+1xOfUuDHOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w
5qxSPTarAgMBAAECggEAZh00uhjFOo35X1TufwSGF0z/c9uMvfMB4i1ufM2qgXud
WXLSLcrksZhhTfLAS4KSTa3PtSKqLBoPg1tdhy9WZqZWxaIxw8ybzaGtn8HNHGyr
LzsVlSLT2ATN4C7VAT9+DeVext0kWHtdz3r5mGagJq2Yx9jRGpQW6rBA9h4ol699
BM09UPCcdlGmpdrb0jDjyfohG139EBSmEeB+Jim+oLO1sXe/LvWllU0UL527CExp
ykiIjASd4s7tFErV9sVJ+bDI97GOyBUGcVMiQ+TRPKFr0kfLgbJz24l8ycPI4odp
IGY+6igicg67n5BktAH+UfCQlUIpWbF2SwRAMht0AQKBgQD8gocy2VuCPj285hBY
8g/1GFd58HkCh54bOhAOb2PK+NE4mRuHCBlBj/tQOmgYz2Pna2k5ldJSUwXsUKkx
9R7hutnwXbcQTSQIRcjhYDLeGetJYXR96ylDig+6XjdW3A5SIc2JzlbVThP39TTm
gRqE/rj9G4ARMfHxffp7YT5AqwKBgQDEuN0pYMKjaW0xvc7WYUOqGHqt2di/BwMr
Ur438MtePArELY35P6kDcrfnlacDToA3Tebk9Rw18y1kl3BFO7VdJbQJSa6RWbp5
aK7E5lq1pCrdyhGwiaI1f5VgzeY8ywS3TqGqU9GOqpENiZqgs1ly9l8gZSaw8/yF
uDWGg7jiAQKBgQCyLtGEmkiuoYkjUR1cBoQoKeMgkwZxOI3jHJfT99ptkiLhU3lP
UfGwiA+JT43BZCdVWEBKeGSP3zIgzdJ3BEekdhvwN9FEWYsBo2zbTOzYOWYExBZV
/KmDlVr/4hge3O3mGyBVDBvOLWh94rRPq+6wxqZ3RP6cI6hdBs7IXZh2PQKBgQDB
rav4kA4xKpvaDCC2yj3/Gmi1/zO5J2NEZQtoMgdXeM+0w5Dy4204Otq7A4jR5Ziw
Wl9H7dZfe1Kmpb5gO1/dHEC7oDJhYjEIVTs0GgMWsFGP2OE/qNHtz/W2wCC8m7jB
7IWYFzvLNTzoUiDNtKYNXGjdkRjdwOlOkcUI8Wi2AQKBgQC9EJsMz/ySt58IvwWy
fQJyg742j21pXHqlMnmHygnSgNa7f3yPQK3FxjvhIPmgu7x8+sSUtXHOjKhZML3p
SdTm/yN487hOYp03jy/wVXLcCDp9XhBeIt/z/TZMPMjAHOLG9xG6cF8AOVq7mLBc
tsDWUHoXPZj/YciXZLq3fPuXyw==
-----END PRIVATE KEY-----

View File

@ -30,7 +30,7 @@ import (
"log"
"net"
"net/http"
_ "net/http/pprof"
_ "net/http/pprof" // HTTP performance profiling, added transparently to HTTP APIs
"os"
"runtime"
"time"
@ -42,6 +42,7 @@ import (
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/publisher"
"github.com/letsencrypt/boulder/va"
)
// Config stores configuration parameters that applications
@ -114,11 +115,7 @@ type Config struct {
VA struct {
UserAgent string
PortConfig struct {
SimpleHTTPPort int
SimpleHTTPSPort int
DVSNIPort int
}
PortConfig va.PortConfig
MaxConcurrentRPCServerRequests int64
@ -220,6 +217,9 @@ type Config struct {
SubscriberAgreementURL string
}
// CAConfig structs have configuration information for the certificate
// authority, including database parameters as well as controls for
// issued certificates.
type CAConfig struct {
Profile string
TestMode bool
@ -242,6 +242,8 @@ type CAConfig struct {
DebugAddr string
}
// PAConfig specifies how a policy authority should connect to its
// database, and what policies it should enforce.
type PAConfig struct {
DBConnect string
EnforcePolicyWhitelist bool
@ -301,6 +303,7 @@ type AppShell struct {
App *cli.App
}
// Version returns a string representing the version of boulder running.
func Version() string {
return fmt.Sprintf("0.1.0 [%s]", core.GetBuildID())
}
@ -412,6 +415,11 @@ func LoadCert(path string) (cert []byte, err error) {
return
}
// DebugServer starts a server to receive debug information. Typical
// usage is to start it in a goroutine, configured with an address
// from the appropriate configuration object:
//
// go cmd.DebugServer(c.XA.DebugAddr)
func DebugServer(addr string) {
if addr == "" {
log.Fatalf("unable to boot debug server because no address was given for it. Set debugAddr.")
@ -424,12 +432,19 @@ func DebugServer(addr string) {
log.Println(http.Serve(ln, nil))
}
// ConfigDuration is just an alias for time.Duration that allows
// serialization to YAML as well as JSON.
type ConfigDuration struct {
time.Duration
}
// ErrDurationMustBeString is returned when a non-string value is
// presented to be deserialized as a ConfigDuration
var ErrDurationMustBeString = errors.New("cannot JSON unmarshal something other than a string into a ConfigDuration")
// UnmarshalJSON parses a string into a ConfigDuration using
// time.ParseDuration. If the input does not unmarshal as a
// string, then UnmarshalJSON returns ErrDurationMustBeString.
func (d *ConfigDuration) UnmarshalJSON(b []byte) error {
s := ""
err := json.Unmarshal(b, &s)
@ -444,10 +459,13 @@ func (d *ConfigDuration) UnmarshalJSON(b []byte) error {
return err
}
// MarshalJSON returns the string form of the duration, as a byte array.
func (d ConfigDuration) MarshalJSON() ([]byte, error) {
return []byte(d.Duration.String()), nil
}
// UnmarshalYAML uses the same frmat as JSON, but is called by the YAML
// parser (vs. the JSON parser).
func (d *ConfigDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {

View File

@ -5,31 +5,45 @@
package core
// SimpleHTTPChallenge constructs a random HTTP challenge
func SimpleHTTPChallenge() Challenge {
tls := true
import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
)
func newChallenge(challengeType string, accountKey *jose.JsonWebKey) Challenge {
return Challenge{
Type: ChallengeTypeSimpleHTTP,
Status: StatusPending,
Token: NewToken(),
TLS: &tls,
Type: challengeType,
Status: StatusPending,
AccountKey: accountKey,
Token: NewToken(),
}
}
// SimpleHTTPChallenge constructs a random HTTP challenge
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func SimpleHTTPChallenge(accountKey *jose.JsonWebKey) Challenge {
challenge := newChallenge(ChallengeTypeSimpleHTTP, accountKey)
tls := true
challenge.TLS = &tls
return challenge
}
// DvsniChallenge constructs a random DVSNI challenge
func DvsniChallenge() Challenge {
return Challenge{
Type: ChallengeTypeDVSNI,
Status: StatusPending,
Token: NewToken(),
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func DvsniChallenge(accountKey *jose.JsonWebKey) Challenge {
return newChallenge(ChallengeTypeDVSNI, accountKey)
}
// DNSChallenge constructs a random DNS challenge
func DNSChallenge() Challenge {
return Challenge{
Type: ChallengeTypeDNS,
Status: StatusPending,
Token: NewToken(),
}
// HTTPChallenge01 constructs a random http-01 challenge
func HTTPChallenge01(accountKey *jose.JsonWebKey) Challenge {
return newChallenge(ChallengeTypeHTTP01, accountKey)
}
// TLSSNIChallenge01 constructs a random tls-sni-00 challenge
func TLSSNIChallenge01(accountKey *jose.JsonWebKey) Challenge {
return newChallenge(ChallengeTypeTLSSNI01, accountKey)
}
// DNSChallenge01 constructs a random DNS challenge
func DNSChallenge01(accountKey *jose.JsonWebKey) Challenge {
return newChallenge(ChallengeTypeDNS01, accountKey)
}

View File

@ -13,22 +13,48 @@ import (
"fmt"
"testing"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
)
// challenges.go
var accountKeyJSON = `{
"kty":"RSA",
"n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ",
"e":"AQAB"
}`
func TestChallenges(t *testing.T) {
simpleHTTP := SimpleHTTPChallenge()
if simpleHTTP.Status != StatusPending {
t.Errorf("Incorrect status for challenge: %v", simpleHTTP.Status)
}
if len(simpleHTTP.Token) != 43 {
t.Errorf("Incorrect length for simpleHTTP token: %v", simpleHTTP.Token)
var accountKey *jose.JsonWebKey
err := json.Unmarshal([]byte(accountKeyJSON), &accountKey)
if err != nil {
t.Errorf("Error unmarshaling JWK: %v", err)
}
dvsni := DvsniChallenge()
if dvsni.Status != StatusPending {
t.Errorf("Incorrect status for challenge: %v", dvsni.Status)
simpleHTTP := SimpleHTTPChallenge(accountKey)
if !simpleHTTP.IsSane(false) {
t.Errorf("New HTTP challenge is not sane: %v", simpleHTTP)
}
dvsni := DvsniChallenge(accountKey)
if !dvsni.IsSane(false) {
t.Errorf("New DVSNI challenge is not sane: %v", dvsni)
}
http01 := HTTPChallenge01(accountKey)
if !http01.IsSane(false) {
t.Errorf("New http-01 challenge is not sane: %v", http01)
}
tlssni01 := TLSSNIChallenge01(accountKey)
if !tlssni01.IsSane(false) {
t.Errorf("New tls-sni-01 challenge is not sane: %v", tlssni01)
}
dns01 := DNSChallenge01(accountKey)
if !dns01.IsSane(false) {
t.Errorf("New dns-01 challenge is not sane: %v", dns01)
}
}
@ -101,8 +127,8 @@ func TestMergeChallenge(t *testing.T) {
if probe.Validated != merged.Validated {
t.Errorf("MergeChallenge allowed response to overwrite completed time")
}
if probe.Token != merged.Token {
t.Errorf("MergeChallenge allowed response to overwrite status")
if probe.KeyAuthorization != merged.KeyAuthorization {
t.Errorf("MergeChallenge allowed response to overwrite authorized key")
}
if probe.TLS != merged.TLS {
t.Errorf("MergeChallenge failed to overwrite TLS")

View File

@ -98,7 +98,7 @@ type CertificateAuthority interface {
// PolicyAuthority defines the public interface for the Boulder PA
type PolicyAuthority interface {
WillingToIssue(id AcmeIdentifier, regID int64) error
ChallengesFor(AcmeIdentifier) ([]Challenge, [][]int)
ChallengesFor(AcmeIdentifier, *jose.JsonWebKey) ([]Challenge, [][]int, error)
}
// StorageGetter are the Boulder SA's read-only methods

View File

@ -6,6 +6,7 @@
package core
import (
"crypto/subtle"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
@ -91,11 +92,13 @@ const (
const (
ChallengeTypeSimpleHTTP = "simpleHttp"
ChallengeTypeDVSNI = "dvsni"
ChallengeTypeDNS = "dns"
ChallengeTypeHTTP01 = "http-01"
ChallengeTypeTLSSNI01 = "tls-sni-01"
ChallengeTypeDNS01 = "dns-01"
)
// The suffix appended to pseudo-domain names in DVSNI challenges
const DVSNISuffix = "acme.invalid"
const TLSSNISuffix = "acme.invalid"
// The label attached to DNS names in DNS challenges
const DNSPrefix = "_acme-challenge"
@ -192,6 +195,97 @@ type ValidationRecord struct {
AddressUsed net.IP `json:"addressUsed"`
}
// KeyAuthorization represents a domain holder's authorization for a
// specific account key to satisfy a specific challenge.
type KeyAuthorization struct {
Token string
Thumbprint string
}
// NewKeyAuthorization computes the thumbprint and assembles the object
func NewKeyAuthorization(token string, key *jose.JsonWebKey) (KeyAuthorization, error) {
if key == nil {
return KeyAuthorization{}, fmt.Errorf("Cannot authorize a nil key")
}
thumbprint, err := Thumbprint(key)
if err != nil {
return KeyAuthorization{}, err
}
return KeyAuthorization{
Token: token,
Thumbprint: thumbprint,
}, nil
}
// NewKeyAuthorizationFromString parses the string and composes a key authorization struct
func NewKeyAuthorizationFromString(input string) (ka KeyAuthorization, err error) {
parts := strings.Split(input, ".")
if len(parts) != 2 {
err = fmt.Errorf("Invalid key authorization: %d parts", len(parts))
return
} else if !LooksLikeAToken(parts[0]) {
err = fmt.Errorf("Invalid key authorization: malformed token")
return
} else if !LooksLikeAToken(parts[1]) {
// Thumbprints have the same syntax as tokens in boulder
// Both are base64-encoded and 32 octets
err = fmt.Errorf("Invalid key authorization: malformed key thumbprint")
return
}
ka = KeyAuthorization{
Token: parts[0],
Thumbprint: parts[1],
}
return
}
// String produces the string representation of a key authorization
func (ka KeyAuthorization) String() string {
return ka.Token + "." + ka.Thumbprint
}
// Match determines whether this KeyAuthorization matches the given token and key
func (ka KeyAuthorization) Match(token string, key *jose.JsonWebKey) bool {
if key == nil {
return false
}
thumbprint, err := Thumbprint(key)
if err != nil {
return false
}
tokensEqual := subtle.ConstantTimeCompare([]byte(token), []byte(ka.Token))
thumbprintsEqual := subtle.ConstantTimeCompare([]byte(thumbprint), []byte(ka.Thumbprint))
return tokensEqual == 1 && thumbprintsEqual == 1
}
// MarshalJSON packs a key authorization into its string representation
func (ka KeyAuthorization) MarshalJSON() (result []byte, err error) {
return json.Marshal(ka.String())
}
// UnmarshalJSON unpacks a key authorization from a string
func (ka *KeyAuthorization) UnmarshalJSON(data []byte) (err error) {
var str string
err = json.Unmarshal(data, &str)
if err != nil {
return err
}
parsed, err := NewKeyAuthorizationFromString(str)
if err != nil {
return err
}
*ka = parsed
return
}
// Challenge is an aggregate of all data needed for any challenges.
//
// Rather than define individual types for different types of
@ -216,15 +310,18 @@ type Challenge struct {
// A URI to which a response can be POSTed
URI string `json:"uri"`
// Used by simpleHttp, dvsni, and dns challenges
// Used by simpleHttp, http-00, tls-sni-00, and dns-00 challenges
Token string `json:"token,omitempty"`
// Used by simpleHTTP challenges
// Used by simpleHttp challenges
TLS *bool `json:"tls,omitempty"`
// Used by dns and dvsni challenges
// Used by dvsni challenges
Validation *jose.JsonWebSignature `json:"validation,omitempty"`
// Used by http-00, tls-sni-00, and dns-00 challenges
KeyAuthorization *KeyAuthorization `json:"keyAuthorization,omitempty"`
// Contains information about URLs used or redirected to and IPs resolved and
// used
ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"`
@ -249,6 +346,9 @@ func (ch Challenge) RecordsSane() bool {
switch ch.Type {
case ChallengeTypeSimpleHTTP:
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this case
fallthrough
case ChallengeTypeHTTP01:
for _, rec := range ch.ValidationRecord {
if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil ||
len(rec.AddressesResolved) == 0 {
@ -256,6 +356,9 @@ func (ch Challenge) RecordsSane() bool {
}
}
case ChallengeTypeDVSNI:
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this case
fallthrough
case ChallengeTypeTLSSNI01:
if len(ch.ValidationRecord) > 1 {
return false
}
@ -266,16 +369,25 @@ func (ch Challenge) RecordsSane() bool {
ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
return false
}
case ChallengeTypeDNS:
case ChallengeTypeDNS01:
// Nothing for now
}
return true
}
// IsSane checks the sanity of a challenge object before issued to the client
// (completed = false) and before validation (completed = true).
func (ch Challenge) IsSane(completed bool) bool {
// isLegacy returns true if the challenge is of a legacy type (i.e., one defined
// before draft-ietf-acme-acme-00)
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func (ch Challenge) isLegacy() bool {
return (ch.Type == ChallengeTypeSimpleHTTP) ||
(ch.Type == ChallengeTypeDVSNI)
}
// legacyIsSane performs sanity checks for legacy challenge types, which have
// a different structure / logic than current challenges.
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func (ch Challenge) legacyIsSane(completed bool) bool {
if ch.Status != StatusPending {
return false
}
@ -303,9 +415,6 @@ func (ch Challenge) IsSane(completed bool) bool {
return false
}
case ChallengeTypeDVSNI:
// Same as DNS
fallthrough
case ChallengeTypeDNS:
// check extra fields aren't used
if ch.TLS != nil {
return false
@ -330,9 +439,10 @@ func (ch Challenge) IsSane(completed bool) bool {
return true
}
// MergeResponse copies a subset of client-provided data to the current Challenge.
// legacyMergeResponse copies a subset of client-provided data to the current Challenge.
// Note: This method does not update the challenge on the left side of the '.'
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func (ch Challenge) legacyMergeResponse(resp Challenge) Challenge {
switch ch.Type {
case ChallengeTypeSimpleHTTP:
// For simpleHttp, only "tls" is client-provided
@ -345,8 +455,6 @@ func (ch Challenge) MergeResponse(resp Challenge) Challenge {
}
case ChallengeTypeDVSNI:
fallthrough
case ChallengeTypeDNS:
// For dvsni and dns, only "validation" is client-provided
if resp.Validation != nil {
ch.Validation = resp.Validation
@ -356,6 +464,65 @@ func (ch Challenge) MergeResponse(resp Challenge) Challenge {
return ch
}
// IsSane checks the sanity of a challenge object before issued to the client
// (completed = false) and before validation (completed = true).
func (ch Challenge) IsSane(completed bool) bool {
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this branch
if ch.isLegacy() {
return ch.legacyIsSane(completed)
}
if ch.Status != StatusPending {
return false
}
// There always needs to be an account key and token
if ch.AccountKey == nil || !LooksLikeAToken(ch.Token) {
return false
}
// Before completion, the key authorization field should be empty
if !completed && ch.KeyAuthorization != nil {
return false
}
// If the challenge is completed, then there should be a key authorization,
// and it should match the challenge.
if completed {
if ch.KeyAuthorization == nil {
return false
}
if !ch.KeyAuthorization.Match(ch.Token, ch.AccountKey) {
return false
}
}
return true
}
// MergeResponse copies a subset of client-provided data to the current Challenge.
// Note: This method does not update the challenge on the left side of the '.'
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this branch
if ch.isLegacy() {
return ch.legacyMergeResponse(resp)
}
// The only client-provided field is the key authorization, and all current
// challenge types use it.
switch ch.Type {
case ChallengeTypeHTTP01:
fallthrough
case ChallengeTypeTLSSNI01:
fallthrough
case ChallengeTypeDNS01:
ch.KeyAuthorization = resp.KeyAuthorization
}
return ch
}
// Authorization represents the authorization of an account key holder
// to act on behalf of a domain. This struct is intended to be used both
// internally and for JSON marshaling on the wire. Any fields that should be
@ -450,11 +617,6 @@ type Certificate struct {
Expires time.Time `db:"expires"`
}
type IssuedCertIdentifierData struct {
ReversedName string
Serial string
}
// IdentifierData holds information about what certificates are known for a
// given identifier. This is used to present Proof of Posession challenges in
// the case where a certificate already exists. The DB table holding
@ -557,6 +719,8 @@ type OCSPSigningRequest struct {
RevokedAt time.Time
}
// SignedCertificateTimestamp represents objects used by Certificate Transparency
// to demonstrate that a certificate was submitted to a CT log. See RFC 6962.
type SignedCertificateTimestamp struct {
ID int `db:"id"`
// The version of the protocol to which the SCT conforms
@ -577,8 +741,6 @@ type SignedCertificateTimestamp struct {
LockCol int64
}
type RPCSignedCertificateTimestamp SignedCertificateTimestamp
type rawSignedCertificateTimestamp struct {
Version uint8 `json:"sct_version"`
LogID string `json:"id"`
@ -587,6 +749,9 @@ type rawSignedCertificateTimestamp struct {
Extensions string `json:"extensions"`
}
// UnmarshalJSON parses the add-chain response from a CT log. It fills all of
// the fields in the SignedCertificateTimestamp struct except for ID and
// CertificateSerial, which are used for local recordkeeping in the Boulder DB.
func (sct *SignedCertificateTimestamp) UnmarshalJSON(data []byte) error {
var err error
var rawSCT rawSignedCertificateTimestamp
@ -649,20 +814,6 @@ func (sct *SignedCertificateTimestamp) CheckSignature() error {
// RevocationCode is used to specify a certificate revocation reason
type RevocationCode int
type RevocationCodes []RevocationCode
func (rc RevocationCodes) Len() int {
return len(rc)
}
func (rc RevocationCodes) Less(i, j int) bool {
return rc[i] < rc[j]
}
func (rc RevocationCodes) Swap(i, j int) {
rc[i], rc[j] = rc[j], rc[i]
}
// RevocationReasons provides a map from reason code to string explaining the
// code
var RevocationReasons = map[RevocationCode]string{

View File

@ -6,6 +6,8 @@
package core
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"net"
"testing"
@ -40,6 +42,24 @@ func TestRegistrationUpdate(t *testing.T) {
test.Assert(t, reg.Agreement == update.Agreement, "Agreement was not updated")
}
var testKey1, _ = rsa.GenerateKey(rand.Reader, 2048)
var testKey2, _ = rsa.GenerateKey(rand.Reader, 2048)
func TestKeyAuthorization(t *testing.T) {
jwk1 := &jose.JsonWebKey{Key: testKey1.Public()}
jwk2 := &jose.JsonWebKey{Key: testKey2.Public()}
ka1, err := NewKeyAuthorization("99DrlWuy-4Nc82olAy0cK7Shnm4uV32pJovyucGEWME", jwk1)
test.AssertNotError(t, err, "Failed to create a new key authorization")
ka2, err := NewKeyAuthorization("Iy2_-2OA8lyD0lwhmD8dD3TIL3wlNpiUhLTXPJG5qOM", jwk2)
test.AssertNotError(t, err, "Failed to create a new key authorization")
test.Assert(t, ka1.Match(ka1.Token, jwk1), "Authorized key should match itself")
test.Assert(t, !ka1.Match(ka1.Token, jwk2), "Authorized key should not match a different key")
test.Assert(t, !ka1.Match(ka2.Token, jwk1), "Authorized key should not match a different token")
test.Assert(t, !ka1.Match(ka2.Token, jwk2), "Authorized key should not match a completely different key")
}
func TestRecordSanityCheck(t *testing.T) {
rec := []ValidationRecord{
ValidationRecord{
@ -66,7 +86,8 @@ func TestRecordSanityCheck(t *testing.T) {
test.Assert(t, !chall.RecordsSane(), "Record should not be sane")
}
func TestChallengeSanityCheck(t *testing.T) {
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this test
func TestChallengeSanityCheck_Legacy(t *testing.T) {
// Make a temporary account key
var accountKey *jose.JsonWebKey
err := json.Unmarshal([]byte(`{
@ -76,7 +97,7 @@ func TestChallengeSanityCheck(t *testing.T) {
}`), &accountKey)
test.AssertNotError(t, err, "Error unmarshaling JWK")
types := []string{ChallengeTypeSimpleHTTP, ChallengeTypeDVSNI, ChallengeTypeDNS}
types := []string{ChallengeTypeSimpleHTTP, ChallengeTypeDVSNI}
for _, challengeType := range types {
chall := Challenge{
Type: challengeType,
@ -107,7 +128,7 @@ func TestChallengeSanityCheck(t *testing.T) {
AddressUsed: net.IP{127, 0, 0, 1},
}}
test.Assert(t, chall.IsSane(true), "IsSane should be true")
} else if challengeType == ChallengeTypeDVSNI || challengeType == ChallengeTypeDNS {
} else if challengeType == ChallengeTypeDVSNI {
chall.Validation = new(jose.JsonWebSignature)
if challengeType == ChallengeTypeDVSNI {
chall.ValidationRecord = []ValidationRecord{ValidationRecord{
@ -128,6 +149,43 @@ func TestChallengeSanityCheck(t *testing.T) {
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
}
func TestChallengeSanityCheck(t *testing.T) {
// Make a temporary account key
var accountKey *jose.JsonWebKey
err := json.Unmarshal([]byte(`{
"kty":"RSA",
"n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ",
"e":"AQAB"
}`), &accountKey)
test.AssertNotError(t, err, "Error unmarshaling JWK")
ka, err := NewKeyAuthorization("KQqLsiS5j0CONR_eUXTUSUDNVaHODtc-0pD6ACif7U4", accountKey)
test.AssertNotError(t, err, "Error creating key authorization")
types := []string{ChallengeTypeHTTP01, ChallengeTypeTLSSNI01, ChallengeTypeDNS01}
for _, challengeType := range types {
chall := Challenge{
Type: challengeType,
Status: StatusInvalid,
AccountKey: accountKey,
}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Status = StatusPending
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = ka.Token
test.Assert(t, chall.IsSane(false), "IsSane should be true")
chall.KeyAuthorization = &ka
test.Assert(t, chall.IsSane(true), "IsSane should be true")
}
chall := Challenge{Type: "bogus", Status: StatusPending}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
}
func TestJSONBufferUnmarshal(t *testing.T) {
testStruct := struct {
Buffer JSONBuffer

View File

@ -8,6 +8,7 @@ package core
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
@ -26,6 +27,7 @@ import (
"io/ioutil"
"math/big"
"net/url"
"regexp"
"strings"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
@ -153,6 +155,14 @@ func NewToken() string {
return RandomString(32)
}
var tokenFormat = regexp.MustCompile("^[\\w-]{43}$")
// LooksLikeAToken checks whether a string represents a 32-octet value in
// the URL-safe base64 alphabet.
func LooksLikeAToken(token string) bool {
return tokenFormat.MatchString(token)
}
// Fingerprints
// Fingerprint256 produces an unpadded, URL-safe Base64-encoded SHA256 digest
@ -163,6 +173,96 @@ func Fingerprint256(data []byte) string {
return B64enc(d.Sum(nil))
}
// Thumbprint produces a JWK thumbprint [RFC7638] of a JWK.
// XXX(rlb): This is adapted from a PR to go-jose, but we need it here until
// that PR is merged and we update to that version.
// https://github.com/square/go-jose/pull/37
// XXX(rlb): Once that lands, we should replace the digest methods below
// with this standard thumbprint.
const rsaThumbprintTemplate = `{"e":"%s","kty":"RSA","n":"%s"}`
const ecThumbprintTemplate = `{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`
// Get JOSE name of curve
func curveName(crv elliptic.Curve) (string, error) {
switch crv {
case elliptic.P256():
return "P-256", nil
case elliptic.P384():
return "P-384", nil
case elliptic.P521():
return "P-521", nil
default:
return "", fmt.Errorf("square/go-jose: unsupported/unknown elliptic curve")
}
}
// Get size of curve in bytes
func curveSize(crv elliptic.Curve) int {
bits := crv.Params().BitSize
div := bits / 8
mod := bits % 8
if mod == 0 {
return div
}
return div + 1
}
func newFixedSizeBuffer(data []byte, length int) []byte {
if len(data) > length {
panic("square/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)")
}
pad := make([]byte, length-len(data))
return append(pad, data...)
}
func ecThumbprintInput(curve elliptic.Curve, x, y *big.Int) (string, error) {
coordLength := curveSize(curve)
crv, err := curveName(curve)
if err != nil {
return "", err
}
return fmt.Sprintf(ecThumbprintTemplate, crv,
B64enc(newFixedSizeBuffer(x.Bytes(), coordLength)),
B64enc(newFixedSizeBuffer(y.Bytes(), coordLength))), nil
}
func rsaThumbprintInput(n *big.Int, e int) (string, error) {
return fmt.Sprintf(rsaThumbprintTemplate,
B64enc(big.NewInt(int64(e)).Bytes()),
B64enc(n.Bytes())), nil
}
// Thumbprint computes the JWK Thumbprint of a key using the
// indicated hash algorithm.
func Thumbprint(k *jose.JsonWebKey) (string, error) {
var input string
var err error
switch key := k.Key.(type) {
case *ecdsa.PublicKey:
input, err = ecThumbprintInput(key.Curve, key.X, key.Y)
case *ecdsa.PrivateKey:
input, err = ecThumbprintInput(key.Curve, key.X, key.Y)
case *rsa.PublicKey:
input, err = rsaThumbprintInput(key.N, key.E)
case *rsa.PrivateKey:
input, err = rsaThumbprintInput(key.N, key.E)
default:
return "", fmt.Errorf("square/go-jose: unkown key type")
}
if err != nil {
return "", err
}
h := sha256.New()
h.Write([]byte(input))
return B64enc(h.Sum(nil)), nil
}
// KeyDigest produces a padded, standard Base64-encoded SHA256 digest of a
// provided public key.
func KeyDigest(key crypto.PublicKey) (string, error) {
@ -201,6 +301,7 @@ func KeyDigestEquals(j, k crypto.PublicKey) bool {
// AcmeURL is a URL that automatically marshal/unmarshal to JSON strings
type AcmeURL url.URL
// ParseAcmeURL is just a wrapper around url.Parse that returns an *AcmeURL
func ParseAcmeURL(s string) (*AcmeURL, error) {
u, err := url.Parse(s)
if err != nil {
@ -327,6 +428,9 @@ func StringToSerial(serial string) (*big.Int, error) {
return &serialNum, err
}
// ValidSerial tests whether the input string represents a syntactically
// valid serial number, i.e., that it is a valid hex string between 32
// and 36 characters long.
func ValidSerial(serial string) bool {
// Originally, serial numbers were 32 hex characters long. We later increased
// them to 36, but we allow the shorter ones because they exist in some

View File

@ -35,6 +35,12 @@ func TestNewToken(t *testing.T) {
return
}
func TestLooksLikeAToken(t *testing.T) {
test.Assert(t, !LooksLikeAToken("R-UL_7MrV3tUUjO9v5ym2srK3dGGCwlxbVyKBdwLOS"), "Accepted short token")
test.Assert(t, !LooksLikeAToken("R-UL_7MrV3tUUjO9v5ym2srK3dGGCwlxbVyKBdwLOS%"), "Accepted invalid token")
test.Assert(t, LooksLikeAToken("R-UL_7MrV3tUUjO9v5ym2srK3dGGCwlxbVyKBdwLOSU"), "Rejected valid token")
}
func TestSerialUtils(t *testing.T) {
serial := SerialToString(big.NewInt(100000000000000000))
test.AssertEquals(t, serial, "00000000000000000000016345785d8a0000")
@ -58,12 +64,22 @@ const JWK1JSON = `{
"e": "AQAB"
}`
const JWK1Digest = `ul04Iq07ulKnnrebv2hv3yxCGgVvoHs8hjq2tVKx3mc=`
const JWK1Thumbprint = `-kVpHjJCDNQQk-j9BGMpzHAVCiOqvoTRZB-Ov4CAiM4`
const JWK2JSON = `{
"kty":"RSA",
"n":"yTsLkI8n4lg9UuSKNRC0UPHsVjNdCYk8rGXIqeb_rRYaEev3D9-kxXY8HrYfGkVt5CiIVJ-n2t50BKT8oBEMuilmypSQqJw0pCgtUm-e6Z0Eg3Ly6DMXFlycyikegiZ0b-rVX7i5OCEZRDkENAYwFNX4G7NNCwEZcH7HUMUmty9dchAqDS9YWzPh_dde1A9oy9JMH07nRGDcOzIh1rCPwc71nwfPPYeeS4tTvkjanjeigOYBFkBLQuv7iBB4LPozsGF1XdoKiIIi-8ye44McdhOTPDcQp3xKxj89aO02pQhBECv61rmbPinvjMG9DYxJmZvjsKF4bN2oy0DxdC1jDw",
"e":"AQAB"
}`
func TestKeyThumbprint(t *testing.T) {
var jwk jose.JsonWebKey
json.Unmarshal([]byte(JWK1JSON), &jwk)
thumbprint, err := Thumbprint(&jwk)
test.AssertNotError(t, err, "Failed to compute JWK digest")
test.AssertEquals(t, thumbprint, JWK1Thumbprint)
}
func TestKeyDigest(t *testing.T) {
// Test with JWK (value, reference, and direct)
var jwk jose.JsonWebKey

View File

@ -13,10 +13,10 @@ import (
blog "github.com/letsencrypt/boulder/log"
)
// MockSyslogWriter implements the blog.SyslogWriter interface. It
// SyslogWriter implements the blog.SyslogWriter interface. It
// stores all logged messages in a buffer for inspection by test
// functions (via GetAll()) instead of sending them to syslog.
type MockSyslogWriter struct {
type SyslogWriter struct {
logged []*LogMessage
msgChan chan<- *LogMessage
getChan <-chan []*LogMessage
@ -24,7 +24,7 @@ type MockSyslogWriter struct {
closeChan chan<- struct{}
}
// LogMessage is a log entry that has been sent to a MockSyslogWriter.
// LogMessage is a log entry that has been sent to a SyslogWriter.
type LogMessage struct {
Priority syslog.Priority // aka Log level
Message string // content of log message
@ -54,19 +54,19 @@ func (lm *LogMessage) String() string {
// // ...
// Assert(t, len(log.GetAll()) > 0, "Should have logged something")
// }
func UseMockLog() *MockSyslogWriter {
func UseMockLog() *SyslogWriter {
sw := NewSyslogWriter()
blog.GetAuditLogger().SyslogWriter = sw
return sw
}
// NewSyslogWriter returns a new MockSyslogWriter.
func NewSyslogWriter() *MockSyslogWriter {
// NewSyslogWriter returns a new SyslogWriter.
func NewSyslogWriter() *SyslogWriter {
msgChan := make(chan *LogMessage)
getChan := make(chan []*LogMessage)
clearChan := make(chan struct{})
closeChan := make(chan struct{})
msw := &MockSyslogWriter{
msw := &SyslogWriter{
logged: []*LogMessage{},
msgChan: msgChan,
getChan: getChan,
@ -91,7 +91,7 @@ func NewSyslogWriter() *MockSyslogWriter {
return msw
}
func (msw *MockSyslogWriter) write(m string, priority syslog.Priority) error {
func (msw *SyslogWriter) write(m string, priority syslog.Priority) error {
msw.msgChan <- &LogMessage{Message: m, Priority: priority}
return nil
}
@ -100,7 +100,7 @@ func (msw *MockSyslogWriter) write(m string, priority syslog.Priority) error {
// Clear(), if applicable).
//
// The caller must not modify the returned slice or its elements.
func (msw *MockSyslogWriter) GetAll() []*LogMessage {
func (msw *SyslogWriter) GetAll() []*LogMessage {
return <-msw.getChan
}
@ -110,7 +110,7 @@ func (msw *MockSyslogWriter) GetAll() []*LogMessage {
// is more important than performance.
//
// The caller must not modify the elements of the returned slice.
func (msw *MockSyslogWriter) GetAllMatching(reString string) (matches []*LogMessage) {
func (msw *SyslogWriter) GetAllMatching(reString string) (matches []*LogMessage) {
re := regexp.MustCompile(reString)
for _, logMsg := range <-msw.getChan {
if re.MatchString(logMsg.Message) {
@ -121,52 +121,52 @@ func (msw *MockSyslogWriter) GetAllMatching(reString string) (matches []*LogMess
}
// Clear resets the log buffer.
func (msw *MockSyslogWriter) Clear() {
func (msw *SyslogWriter) Clear() {
msw.clearChan <- struct{}{}
}
// Close releases resources. No other methods may be called after this.
func (msw *MockSyslogWriter) Close() error {
func (msw *SyslogWriter) Close() error {
msw.closeChan <- struct{}{}
return nil
}
// Alert logs at LOG_ALERT
func (msw *MockSyslogWriter) Alert(m string) error {
func (msw *SyslogWriter) Alert(m string) error {
return msw.write(m, syslog.LOG_ALERT)
}
// Crit logs at LOG_CRIT
func (msw *MockSyslogWriter) Crit(m string) error {
func (msw *SyslogWriter) Crit(m string) error {
return msw.write(m, syslog.LOG_CRIT)
}
// Debug logs at LOG_DEBUG
func (msw *MockSyslogWriter) Debug(m string) error {
func (msw *SyslogWriter) Debug(m string) error {
return msw.write(m, syslog.LOG_DEBUG)
}
// Emerg logs at LOG_EMERG
func (msw *MockSyslogWriter) Emerg(m string) error {
func (msw *SyslogWriter) Emerg(m string) error {
return msw.write(m, syslog.LOG_EMERG)
}
// Err logs at LOG_ERR
func (msw *MockSyslogWriter) Err(m string) error {
func (msw *SyslogWriter) Err(m string) error {
return msw.write(m, syslog.LOG_ERR)
}
// Info logs at LOG_INFO
func (msw *MockSyslogWriter) Info(m string) error {
func (msw *SyslogWriter) Info(m string) error {
return msw.write(m, syslog.LOG_INFO)
}
// Notice logs at LOG_NOTICE
func (msw *MockSyslogWriter) Notice(m string) error {
func (msw *SyslogWriter) Notice(m string) error {
return msw.write(m, syslog.LOG_NOTICE)
}
// Warning logs at LOG_WARNING
func (msw *MockSyslogWriter) Warning(m string) error {
func (msw *SyslogWriter) Warning(m string) error {
return msw.write(m, syslog.LOG_WARNING)
}

View File

@ -20,17 +20,17 @@ import (
"github.com/letsencrypt/boulder/core"
)
// MockDNS is a mock
type MockDNS struct {
// DNSResolver is a mock
type DNSResolver struct {
}
// ExchangeOne is a mock
func (mock *MockDNS) ExchangeOne(hostname string, qt uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
func (mock *DNSResolver) ExchangeOne(hostname string, qt uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
return nil, 0, nil
}
// LookupTXT is a mock
func (mock *MockDNS) LookupTXT(hostname string) ([]string, time.Duration, error) {
func (mock *DNSResolver) LookupTXT(hostname string) ([]string, time.Duration, error) {
if hostname == "_acme-challenge.servfail.com" {
return nil, 0, fmt.Errorf("SERVFAIL")
}
@ -38,7 +38,7 @@ func (mock *MockDNS) LookupTXT(hostname string) ([]string, time.Duration, error)
}
// LookupHost is a mock
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
func (mock *DNSResolver) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
if hostname == "always.invalid" || hostname == "invalid.invalid" {
return []net.IP{}, 0, nil
}
@ -47,7 +47,7 @@ func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, error
}
// LookupCNAME is a mock
func (mock *MockDNS) LookupCNAME(domain string) (string, time.Duration, error) {
func (mock *DNSResolver) LookupCNAME(domain string) (string, time.Duration, error) {
switch strings.TrimRight(domain, ".") {
case "cname-absent.com":
return "absent.com.", 30, nil
@ -76,7 +76,7 @@ func (mock *MockDNS) LookupCNAME(domain string) (string, time.Duration, error) {
}
// LookupDNAME is a mock
func (mock *MockDNS) LookupDNAME(domain string) (string, time.Duration, error) {
func (mock *DNSResolver) LookupDNAME(domain string) (string, time.Duration, error) {
switch strings.TrimRight(domain, ".") {
case "cname-and-dname.com", "dname-present.com":
return "dname-target.present.com.", time.Minute, nil
@ -94,7 +94,7 @@ func (mock *MockDNS) LookupDNAME(domain string) (string, time.Duration, error) {
}
// LookupCAA is a mock
func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
func (mock *DNSResolver) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
var results []*dns.CAA
var record dns.CAA
switch strings.TrimRight(domain, ".") {
@ -118,7 +118,7 @@ func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error)
}
// LookupMX is a mock
func (mock *MockDNS) LookupMX(domain string) ([]string, time.Duration, error) {
func (mock *DNSResolver) LookupMX(domain string) ([]string, time.Duration, error) {
switch strings.TrimRight(domain, ".") {
case "letsencrypt.org":
fallthrough
@ -128,8 +128,8 @@ func (mock *MockDNS) LookupMX(domain string) ([]string, time.Duration, error) {
return nil, 0, nil
}
// MockSA is a mock
type MockSA struct {
// StorageAuthority is a mock
type StorageAuthority struct {
authorizedDomains map[string]bool
}
@ -149,7 +149,7 @@ const (
)
// GetRegistration is a mock
func (sa *MockSA) GetRegistration(id int64) (core.Registration, error) {
func (sa *StorageAuthority) GetRegistration(id int64) (core.Registration, error) {
if id == 100 {
// Tag meaning "Missing"
return core.Registration{}, errors.New("missing")
@ -167,7 +167,7 @@ func (sa *MockSA) GetRegistration(id int64) (core.Registration, error) {
}
// GetRegistrationByKey is a mock
func (sa *MockSA) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) {
func (sa *StorageAuthority) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) {
var test1KeyPublic jose.JsonWebKey
var test2KeyPublic jose.JsonWebKey
test1KeyPublic.UnmarshalJSON([]byte(test1KeyPublicJSON))
@ -187,7 +187,7 @@ func (sa *MockSA) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration,
}
// GetAuthorization is a mock
func (sa *MockSA) GetAuthorization(id string) (core.Authorization, error) {
func (sa *StorageAuthority) GetAuthorization(id string) (core.Authorization, error) {
if id == "valid" {
exp := time.Now().AddDate(100, 0, 0)
return core.Authorization{
@ -209,7 +209,7 @@ func (sa *MockSA) GetAuthorization(id string) (core.Authorization, error) {
}
// GetCertificate is a mock
func (sa *MockSA) GetCertificate(serial string) (core.Certificate, error) {
func (sa *StorageAuthority) GetCertificate(serial string) (core.Certificate, error) {
// Serial ee == 238.crt
if serial == "0000000000000000000000000000000000ee" {
certPemBytes, _ := ioutil.ReadFile("test/238.crt")
@ -231,7 +231,7 @@ func (sa *MockSA) GetCertificate(serial string) (core.Certificate, error) {
}
// GetCertificateStatus is a mock
func (sa *MockSA) GetCertificateStatus(serial string) (core.CertificateStatus, error) {
func (sa *StorageAuthority) GetCertificateStatus(serial string) (core.CertificateStatus, error) {
// Serial ee == 238.crt
if serial == "0000000000000000000000000000000000ee" {
return core.CertificateStatus{
@ -247,57 +247,57 @@ func (sa *MockSA) GetCertificateStatus(serial string) (core.CertificateStatus, e
}
// AlreadyDeniedCSR is a mock
func (sa *MockSA) AlreadyDeniedCSR([]string) (bool, error) {
func (sa *StorageAuthority) AlreadyDeniedCSR([]string) (bool, error) {
return false, nil
}
// AddCertificate is a mock
func (sa *MockSA) AddCertificate(certDER []byte, regID int64) (digest string, err error) {
func (sa *StorageAuthority) AddCertificate(certDER []byte, regID int64) (digest string, err error) {
return
}
// FinalizeAuthorization is a mock
func (sa *MockSA) FinalizeAuthorization(authz core.Authorization) (err error) {
func (sa *StorageAuthority) FinalizeAuthorization(authz core.Authorization) (err error) {
return
}
// MarkCertificateRevoked is a mock
func (sa *MockSA) MarkCertificateRevoked(serial string, ocspResponse []byte, reasonCode core.RevocationCode) (err error) {
func (sa *StorageAuthority) MarkCertificateRevoked(serial string, ocspResponse []byte, reasonCode core.RevocationCode) (err error) {
return
}
// UpdateOCSP is a mock
func (sa *MockSA) UpdateOCSP(serial string, ocspResponse []byte) (err error) {
func (sa *StorageAuthority) UpdateOCSP(serial string, ocspResponse []byte) (err error) {
return
}
// NewPendingAuthorization is a mock
func (sa *MockSA) NewPendingAuthorization(authz core.Authorization) (output core.Authorization, err error) {
func (sa *StorageAuthority) NewPendingAuthorization(authz core.Authorization) (output core.Authorization, err error) {
return
}
// NewRegistration is a mock
func (sa *MockSA) NewRegistration(reg core.Registration) (regR core.Registration, err error) {
func (sa *StorageAuthority) NewRegistration(reg core.Registration) (regR core.Registration, err error) {
return
}
// UpdatePendingAuthorization is a mock
func (sa *MockSA) UpdatePendingAuthorization(authz core.Authorization) (err error) {
func (sa *StorageAuthority) UpdatePendingAuthorization(authz core.Authorization) (err error) {
return
}
// UpdateRegistration is a mock
func (sa *MockSA) UpdateRegistration(reg core.Registration) (err error) {
func (sa *StorageAuthority) UpdateRegistration(reg core.Registration) (err error) {
return
}
// GetSCTReceipt is a mock
func (sa *MockSA) GetSCTReceipt(serial string, logID string) (sct core.SignedCertificateTimestamp, err error) {
func (sa *StorageAuthority) GetSCTReceipt(serial string, logID string) (sct core.SignedCertificateTimestamp, err error) {
return
}
// AddSCTReceipt is a mock
func (sa *MockSA) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) {
func (sa *StorageAuthority) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) {
if sct.Signature == nil {
err = fmt.Errorf("Bad times")
}
@ -305,8 +305,8 @@ func (sa *MockSA) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error)
}
// GetLatestValidAuthorization is a mock
func (sa *MockSA) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
if registrationId == 1 && identifier.Type == "dns" {
func (sa *StorageAuthority) GetLatestValidAuthorization(registrationID int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
if registrationID == 1 && identifier.Type == "dns" {
if sa.authorizedDomains[identifier.Value] || identifier.Value == "not-an-example.com" {
exp := time.Now().AddDate(100, 0, 0)
return core.Authorization{Status: core.StatusValid, RegistrationID: 1, Expires: &exp, Identifier: identifier}, nil
@ -316,21 +316,21 @@ func (sa *MockSA) GetLatestValidAuthorization(registrationId int64, identifier c
}
// CountCertificatesRange is a mock
func (sa *MockSA) CountCertificatesRange(_, _ time.Time) (int64, error) {
func (sa *StorageAuthority) CountCertificatesRange(_, _ time.Time) (int64, error) {
return 0, nil
}
// CountCertificatesByNames is a mock
func (sa *MockSA) CountCertificatesByNames(_ []string, _, _ time.Time) (ret map[string]int, err error) {
func (sa *StorageAuthority) CountCertificatesByNames(_ []string, _, _ time.Time) (ret map[string]int, err error) {
return
}
// MockPublisher is a mock
type MockPublisher struct {
// Publisher is a mock
type Publisher struct {
// empty
}
// SubmitToCT is a mock
func (*MockPublisher) SubmitToCT([]byte) error {
func (*Publisher) SubmitToCT([]byte) error {
return nil
}

View File

@ -11,6 +11,7 @@ import (
"regexp"
"strings"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
@ -198,14 +199,14 @@ func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64
// 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) {
func (pa PolicyAuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier, accountKey *jose.JsonWebKey) (challenges []core.Challenge, combinations [][]int, err error) {
// TODO(https://github.com/letsencrypt/boulder/issues/894): Update these lines
challenges = []core.Challenge{
core.SimpleHTTPChallenge(),
core.DvsniChallenge(),
}
combinations = [][]int{
[]int{0},
[]int{1},
core.SimpleHTTPChallenge(accountKey),
core.DvsniChallenge(accountKey),
core.HTTPChallenge01(accountKey),
core.TLSSNIChallenge01(accountKey),
}
combinations = [][]int{[]int{0}, []int{1}, []int{2}, []int{3}}
return
}

View File

@ -6,8 +6,11 @@
package policy
import (
"encoding/json"
"testing"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
@ -167,17 +170,38 @@ func TestWillingToIssue(t *testing.T) {
}
}
var accountKeyJSON = `{
"kty":"RSA",
"n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ",
"e":"AQAB"
}`
func TestChallengesFor(t *testing.T) {
pa, cleanup := paImpl(t)
defer cleanup()
challenges, combinations := pa.ChallengesFor(core.AcmeIdentifier{})
var accountKey *jose.JsonWebKey
err := json.Unmarshal([]byte(accountKeyJSON), &accountKey)
if err != nil {
t.Errorf("Error unmarshaling JWK: %v", err)
}
if len(challenges) != 2 || challenges[0].Type != core.ChallengeTypeSimpleHTTP ||
challenges[1].Type != core.ChallengeTypeDVSNI {
challenges, combinations, err := pa.ChallengesFor(core.AcmeIdentifier{}, accountKey)
if err != nil {
t.Errorf("Error generating challenges: %v", err)
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Update these tests
if len(challenges) != 4 ||
challenges[0].Type != core.ChallengeTypeSimpleHTTP ||
challenges[1].Type != core.ChallengeTypeDVSNI ||
challenges[2].Type != core.ChallengeTypeHTTP01 ||
challenges[3].Type != core.ChallengeTypeTLSSNI01 {
t.Error("Incorrect challenges returned")
}
if len(combinations) != 2 || combinations[0][0] != 0 || combinations[1][0] != 1 {
if len(combinations) != 4 ||
combinations[0][0] != 0 || combinations[1][0] != 1 ||
combinations[2][0] != 2 || combinations[3][0] != 3 {
t.Error("Incorrect combinations returned")
}
}

View File

@ -24,6 +24,7 @@ import (
"github.com/letsencrypt/boulder/sa"
)
// LogDescription tells you how to connect to a log and verify its statements.
type LogDescription struct {
ID string
URI string
@ -35,6 +36,9 @@ type rawLogDescription struct {
PublicKey string `json:"key"`
}
// UnmarshalJSON parses a simple JSON format for log descriptions. Both the
// URI and the public key are expected to be strings. The public key is a
// base64-encoded PKIX public key structure.
func (logDesc *LogDescription) UnmarshalJSON(data []byte) error {
var rawLogDesc rawLogDescription
if err := json.Unmarshal(data, &rawLogDesc); err != nil {

View File

@ -196,7 +196,7 @@ func setup(t *testing.T, port, retries int) (PublisherImpl, *x509.Certificate) {
})
test.AssertNotError(t, err, "Couldn't create new Publisher")
pub.issuerBundle = append(pub.issuerBundle, base64.StdEncoding.EncodeToString(intermediatePEM.Bytes))
pub.SA = &mocks.MockSA{}
pub.SA = &mocks.StorageAuthority{}
leafPEM, _ := pem.Decode([]byte(testLeaf))
leaf, err := x509.ParseCertificate(leafPEM.Bytes)

View File

@ -214,13 +214,8 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
return authz, err
}
// Create validations, but we have to update them with URIs later
challenges, combinations := ra.PA.ChallengesFor(identifier)
for i := range challenges {
// Add the account key used to generate the challenge
challenges[i].AccountKey = &reg.Key
}
// Create validations. The WFE will update them with URIs before sending them out.
challenges, combinations, err := ra.PA.ChallengesFor(identifier, &reg.Key)
expires := ra.clk.Now().Add(ra.authorizationLifetime)
@ -578,6 +573,12 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(base core.Authorization
}
authz.Challenges[challengeIndex] = authz.Challenges[challengeIndex].MergeResponse(response)
// At this point, the challenge should be sane as a complete challenge
if !authz.Challenges[challengeIndex].IsSane(true) {
err = core.MalformedRequestError("Response does not complete challenge")
return
}
// Store the updated version
if err = ra.SA.UpdatePendingAuthorization(authz); err != nil {
// This can pretty much only happen when the client corrupts the Challenge

View File

@ -101,9 +101,6 @@ var (
}
ResponseIndex = 0
Response = core.Challenge{
Type: "simpleHttp",
}
ExampleCSR = &x509.CertificateRequest{}
@ -116,11 +113,7 @@ var (
Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-example.com"},
RegistrationID: 1,
Status: "pending",
Challenges: []core.Challenge{
core.SimpleHTTPChallenge(),
core.DvsniChallenge(),
},
Combinations: [][]int{[]int{0}, []int{1}},
Combinations: [][]int{[]int{0}, []int{1}},
}
AuthzFinal = core.Authorization{}
@ -132,6 +125,16 @@ const (
saDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test"
)
func makeResponse(ch core.Challenge) (out core.Challenge, err error) {
keyAuthorization, err := core.NewKeyAuthorization(ch.Token, ch.AccountKey)
if err != nil {
return
}
out = core.Challenge{KeyAuthorization: &keyAuthorization}
return
}
func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAuthority, *RegistrationAuthorityImpl, clock.FakeClock, func()) {
err := json.Unmarshal(AccountKeyJSONA, &AccountKeyA)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
@ -144,7 +147,11 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
test.AssertNotError(t, err, "Failed to unmarshal private JWK")
err = json.Unmarshal(ShortKeyJSON, &ShortKey)
test.AssertNotError(t, err, "Failed to unmarshall JWK")
test.AssertNotError(t, err, "Failed to unmarshal JWK")
simpleHTTP := core.SimpleHTTPChallenge(&AccountKeyA)
dvsni := core.DvsniChallenge(&AccountKeyA)
AuthzInitial.Challenges = []core.Challenge{simpleHTTP, dvsni}
fc := clock.NewFake()
@ -195,7 +202,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
ValidityPeriod: time.Hour * 2190,
NotAfter: time.Now().Add(time.Hour * 8761),
Clk: fc,
Publisher: &mocks.MockPublisher{},
Publisher: &mocks.Publisher{},
}
cleanUp := func() {
saDBCleanUp()
@ -218,7 +225,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
ra.VA = va
ra.CA = &ca
ra.PA = pa
ra.DNSResolver = &mocks.MockDNS{}
ra.DNSResolver = &mocks.DNSResolver{}
AuthzInitial.RegistrationID = Registration.ID
@ -256,38 +263,38 @@ func TestValidateContacts(t *testing.T) {
nStats, _ := statsd.NewNoopClient()
err := validateContacts([]*core.AcmeURL{}, &mocks.MockDNS{}, nStats)
err := validateContacts([]*core.AcmeURL{}, &mocks.DNSResolver{}, nStats)
test.AssertNotError(t, err, "No Contacts")
err = validateContacts([]*core.AcmeURL{tel}, &mocks.MockDNS{}, nStats)
err = validateContacts([]*core.AcmeURL{tel}, &mocks.DNSResolver{}, nStats)
test.AssertNotError(t, err, "Simple Telephone")
err = validateContacts([]*core.AcmeURL{validEmail}, &mocks.MockDNS{}, nStats)
err = validateContacts([]*core.AcmeURL{validEmail}, &mocks.DNSResolver{}, nStats)
test.AssertNotError(t, err, "Valid Email")
err = validateContacts([]*core.AcmeURL{invalidEmail}, &mocks.MockDNS{}, nStats)
err = validateContacts([]*core.AcmeURL{invalidEmail}, &mocks.DNSResolver{}, nStats)
test.AssertError(t, err, "Invalid Email")
err = validateContacts([]*core.AcmeURL{malformedEmail}, &mocks.MockDNS{}, nStats)
err = validateContacts([]*core.AcmeURL{malformedEmail}, &mocks.DNSResolver{}, nStats)
test.AssertError(t, err, "Malformed Email")
err = validateContacts([]*core.AcmeURL{ansible}, &mocks.MockDNS{}, nStats)
err = validateContacts([]*core.AcmeURL{ansible}, &mocks.DNSResolver{}, nStats)
test.AssertError(t, err, "Unknown scehme")
}
func TestValidateEmail(t *testing.T) {
_, err := validateEmail("an email`", &mocks.MockDNS{})
_, err := validateEmail("an email`", &mocks.DNSResolver{})
test.AssertError(t, err, "Malformed")
_, err = validateEmail("a@not.a.domain", &mocks.MockDNS{})
_, err = validateEmail("a@not.a.domain", &mocks.DNSResolver{})
test.AssertError(t, err, "Cannot resolve")
t.Logf("No Resolve: %s", err)
_, err = validateEmail("a@example.com", &mocks.MockDNS{})
_, err = validateEmail("a@example.com", &mocks.DNSResolver{})
test.AssertError(t, err, "No MX Record")
t.Logf("No MX: %s", err)
_, err = validateEmail("a@email.com", &mocks.MockDNS{})
_, err = validateEmail("a@email.com", &mocks.DNSResolver{})
test.AssertNotError(t, err, "Valid")
}
@ -377,12 +384,22 @@ func TestNewAuthorization(t *testing.T) {
test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
// TODO Verify that challenges are correct
test.Assert(t, len(authz.Challenges) == 2, "Incorrect number of challenges returned")
// TODO(https://github.com/letsencrypt/boulder/issues/894): Update these lines
test.Assert(t, len(authz.Challenges) == 4, "Incorrect number of challenges returned")
test.Assert(t, authz.Challenges[0].Type == core.ChallengeTypeSimpleHTTP, "Challenge 0 not SimpleHTTP")
test.Assert(t, authz.Challenges[1].Type == core.ChallengeTypeDVSNI, "Challenge 1 not DVSNI")
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete these lines
test.Assert(t, authz.Challenges[2].Type == core.ChallengeTypeHTTP01, "Challenge 2 not http-00")
test.Assert(t, authz.Challenges[3].Type == core.ChallengeTypeTLSSNI01, "Challenge 3 not tlssni-00")
test.Assert(t, authz.Challenges[0].IsSane(false), "Challenge 0 is not sane")
test.Assert(t, authz.Challenges[1].IsSane(false), "Challenge 1 is not sane")
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete these lines
test.Assert(t, authz.Challenges[2].IsSane(false), "Challenge 2 is not sane")
test.Assert(t, authz.Challenges[3].IsSane(false), "Challenge 3 is not sane")
t.Log("DONE TestNewAuthorization")
}
@ -394,7 +411,9 @@ func TestUpdateAuthorization(t *testing.T) {
authz, err := ra.NewAuthorization(AuthzRequest, Registration.ID)
test.AssertNotError(t, err, "NewAuthorization failed")
authz, err = ra.UpdateAuthorization(authz, ResponseIndex, Response)
response, err := makeResponse(authz.Challenges[ResponseIndex])
test.AssertNotError(t, err, "Unable to construct response to challenge")
authz, err = ra.UpdateAuthorization(authz, ResponseIndex, response)
test.AssertNotError(t, err, "UpdateAuthorization failed")
// Verify that returned authz same as DB
@ -428,7 +447,9 @@ func TestUpdateAuthorizationReject(t *testing.T) {
test.AssertNotError(t, err, "UpdateRegistration failed")
// Verify that the RA rejected the authorization request
_, err = ra.UpdateAuthorization(authz, ResponseIndex, Response)
response, err := makeResponse(authz.Challenges[ResponseIndex])
test.AssertNotError(t, err, "Unable to construct response to challenge")
_, err = ra.UpdateAuthorization(authz, ResponseIndex, response)
test.AssertEquals(t, err, core.UnauthorizedError("Challenge cannot be updated with a different key"))
t.Log("DONE TestUpdateAuthorizationReject")
@ -437,7 +458,9 @@ func TestUpdateAuthorizationReject(t *testing.T) {
func TestOnValidationUpdateSuccess(t *testing.T) {
_, sa, ra, fclk, cleanUp := initAuthorities(t)
defer cleanUp()
authzUpdated, _ := sa.NewPendingAuthorization(AuthzInitial)
authzUpdated, err := sa.NewPendingAuthorization(AuthzInitial)
test.AssertNotError(t, err, "Failed to create new pending authz")
expires := fclk.Now().Add(300 * 24 * time.Hour)
authzUpdated.Expires = &expires
sa.UpdatePendingAuthorization(authzUpdated)
@ -632,7 +655,7 @@ func TestDomainsForRateLimiting(t *testing.T) {
}
type mockSAWithNameCounts struct {
mocks.MockSA
mocks.StorageAuthority
nameCounts map[string]int
t *testing.T
clk clock.FakeClock

View File

@ -200,16 +200,16 @@ func (rpc *AmqpRPCServer) Handle(method string, handler func([]byte) ([]byte, er
rpc.dispatchTable[method] = handler
}
// RPCError is a JSON wrapper for error as it cannot be un/marshalled
// rpcError is a JSON wrapper for error as it cannot be un/marshalled
// due to type interface{}.
type RPCError struct {
type rpcError struct {
Value string `json:"value"`
Type string `json:"type,omitempty"`
}
// Wraps a error in a RPCError so it can be marshalled to
// Wraps a error in a rpcError so it can be marshalled to
// JSON.
func wrapError(err error) (rpcError RPCError) {
func wrapError(err error) (rpcError rpcError) {
if err != nil {
rpcError.Value = err.Error()
switch err.(type) {
@ -240,8 +240,8 @@ func wrapError(err error) (rpcError RPCError) {
return
}
// Unwraps a RPCError and returns the correct error type.
func unwrapError(rpcError RPCError) (err error) {
// Unwraps a rpcError and returns the correct error type.
func unwrapError(rpcError rpcError) (err error) {
if rpcError.Value != "" {
switch rpcError.Type {
case "InternalServerError":
@ -273,11 +273,11 @@ func unwrapError(rpcError RPCError) (err error) {
return
}
// RPCResponse is a stuct for wire-representation of response messages
// rpcResponse is a stuct for wire-representation of response messages
// used by DispatchSync
type RPCResponse struct {
type rpcResponse struct {
ReturnVal []byte `json:"returnVal,omitempty"`
Error RPCError `json:"error,omitempty"`
Error rpcError `json:"error,omitempty"`
}
// AmqpChannel sets a AMQP connection up using SSL if configuration is provided
@ -362,7 +362,7 @@ func (rpc *AmqpRPCServer) processMessage(msg amqp.Delivery) {
rpc.log.Audit(fmt.Sprintf(" [s<][%s][%s] Misrouted message: %s - %s - %s", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(msg.Body), msg.CorrelationId))
return
}
var response RPCResponse
var response rpcResponse
var err error
response.ReturnVal, err = cb(msg.Body)
response.Error = wrapError(err)
@ -407,7 +407,7 @@ func (rpc *AmqpRPCServer) replyTooManyRequests(msg amqp.Delivery) {
// until a fatal error is returned or AmqpRPCServer.Stop() is called and all
// remaining messages are processed.
func (rpc *AmqpRPCServer) Start(c cmd.Config) error {
tooManyGoroutines := RPCResponse{
tooManyGoroutines := rpcResponse{
Error: wrapError(core.TooManyRPCRequestsError("RPC server has spawned too many Goroutines")),
}
tooManyRequestsResponse, err := json.Marshal(tooManyGoroutines)
@ -632,7 +632,7 @@ func (rpc *AmqpRPCCLient) DispatchSync(method string, body []byte) (response []b
callStarted := time.Now()
select {
case jsonResponse := <-rpc.Dispatch(method, body):
var rpcResponse RPCResponse
var rpcResponse rpcResponse
err = json.Unmarshal(jsonResponse, &rpcResponse)
if err != nil {
return

View File

@ -9,14 +9,14 @@ import (
"time"
)
// RPCClient describes the functions an RPC Client performs
type RPCClient interface {
// Client describes the functions an RPC Client performs
type Client interface {
SetTimeout(time.Duration)
Dispatch(string, []byte) chan []byte
DispatchSync(string, []byte) ([]byte, error)
}
// RPCServer describes the functions an RPC Server performs
type RPCServer interface {
// Server describes the functions an RPC Server performs
type Server interface {
Handle(string, func([]byte) ([]byte, error))
}

View File

@ -168,7 +168,7 @@ func errorCondition(method string, err error, obj interface{}) {
}
// NewRegistrationAuthorityServer constructs an RPC server
func NewRegistrationAuthorityServer(rpc RPCServer, impl core.RegistrationAuthority) error {
func NewRegistrationAuthorityServer(rpc Server, impl core.RegistrationAuthority) error {
log := blog.GetAuditLogger()
rpc.Handle(MethodNewRegistration, func(req []byte) (response []byte, err error) {
@ -345,11 +345,11 @@ func NewRegistrationAuthorityServer(rpc RPCServer, impl core.RegistrationAuthori
// RegistrationAuthorityClient represents an RA RPC client
type RegistrationAuthorityClient struct {
rpc RPCClient
rpc Client
}
// NewRegistrationAuthorityClient constructs an RPC client
func NewRegistrationAuthorityClient(client RPCClient) (rac RegistrationAuthorityClient, err error) {
func NewRegistrationAuthorityClient(client Client) (rac RegistrationAuthorityClient, err error) {
rac = RegistrationAuthorityClient{rpc: client}
return
}
@ -496,7 +496,7 @@ func (rac RegistrationAuthorityClient) OnValidationUpdate(authz core.Authorizati
//
// ValidationAuthorityClient / Server
// -> UpdateValidations
func NewValidationAuthorityServer(rpc RPCServer, impl core.ValidationAuthority) (err error) {
func NewValidationAuthorityServer(rpc Server, impl core.ValidationAuthority) (err error) {
rpc.Handle(MethodUpdateValidations, func(req []byte) (response []byte, err error) {
var vaReq validationRequest
if err = json.Unmarshal(req, &vaReq); err != nil {
@ -540,11 +540,11 @@ func NewValidationAuthorityServer(rpc RPCServer, impl core.ValidationAuthority)
// ValidationAuthorityClient represents an RPC client for the VA
type ValidationAuthorityClient struct {
rpc RPCClient
rpc Client
}
// NewValidationAuthorityClient constructs an RPC client
func NewValidationAuthorityClient(client RPCClient) (vac ValidationAuthorityClient, err error) {
func NewValidationAuthorityClient(client Client) (vac ValidationAuthorityClient, err error) {
vac = ValidationAuthorityClient{rpc: client}
return
}
@ -589,7 +589,8 @@ func (vac ValidationAuthorityClient) CheckCAARecords(ident core.AcmeIdentifier)
return
}
func NewPublisherServer(rpc RPCServer, impl core.Publisher) (err error) {
// NewPublisherServer creates a new server that wraps a CT publisher
func NewPublisherServer(rpc Server, impl core.Publisher) (err error) {
rpc.Handle(MethodSubmitToCT, func(req []byte) (response []byte, err error) {
err = impl.SubmitToCT(req)
return
@ -600,11 +601,11 @@ func NewPublisherServer(rpc RPCServer, impl core.Publisher) (err error) {
// PublisherClient is a client to communicate with the Publisher Authority
type PublisherClient struct {
rpc RPCClient
rpc Client
}
// NewPublisherClient constructs an RPC client
func NewPublisherClient(client RPCClient) (pub PublisherClient, err error) {
func NewPublisherClient(client Client) (pub PublisherClient, err error) {
pub = PublisherClient{rpc: client}
return
}
@ -619,7 +620,7 @@ func (pub PublisherClient) SubmitToCT(der []byte) (err error) {
//
// CertificateAuthorityClient / Server
// -> IssueCertificate
func NewCertificateAuthorityServer(rpc RPCServer, impl core.CertificateAuthority) (err error) {
func NewCertificateAuthorityServer(rpc Server, impl core.CertificateAuthority) (err error) {
rpc.Handle(MethodIssueCertificate, func(req []byte) (response []byte, err error) {
var icReq issueCertificateRequest
err = json.Unmarshal(req, &icReq)
@ -686,11 +687,11 @@ func NewCertificateAuthorityServer(rpc RPCServer, impl core.CertificateAuthority
// CertificateAuthorityClient is a client to communicate with the CA.
type CertificateAuthorityClient struct {
rpc RPCClient
rpc Client
}
// NewCertificateAuthorityClient constructs an RPC client
func NewCertificateAuthorityClient(client RPCClient) (cac CertificateAuthorityClient, err error) {
func NewCertificateAuthorityClient(client Client) (cac CertificateAuthorityClient, err error) {
cac = CertificateAuthorityClient{rpc: client}
return
}
@ -752,7 +753,7 @@ func (cac CertificateAuthorityClient) GenerateOCSP(signRequest core.OCSPSigningR
}
// NewStorageAuthorityServer constructs an RPC server
func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error {
func NewStorageAuthorityServer(rpc Server, impl core.StorageAuthority) error {
rpc.Handle(MethodUpdateRegistration, func(req []byte) (response []byte, err error) {
var reg core.Registration
if err = json.Unmarshal(req, &reg); err != nil {
@ -1055,7 +1056,7 @@ func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error
}
sct, err := impl.GetSCTReceipt(gsctReq.Serial, gsctReq.LogID)
jsonResponse, err := json.Marshal(core.RPCSignedCertificateTimestamp(sct))
jsonResponse, err := json.Marshal(core.SignedCertificateTimestamp(sct))
if err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
errorCondition(MethodGetSCTReceipt, err, req)
@ -1066,7 +1067,7 @@ func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error
})
rpc.Handle(MethodAddSCTReceipt, func(req []byte) (response []byte, err error) {
var sct core.RPCSignedCertificateTimestamp
var sct core.SignedCertificateTimestamp
err = json.Unmarshal(req, &sct)
if err != nil {
// AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64
@ -1089,11 +1090,11 @@ func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error
// StorageAuthorityClient is a client to communicate with the Storage Authority
type StorageAuthorityClient struct {
rpc RPCClient
rpc Client
}
// NewStorageAuthorityClient constructs an RPC client
func NewStorageAuthorityClient(client RPCClient) (sac StorageAuthorityClient, err error) {
func NewStorageAuthorityClient(client Client) (sac StorageAuthorityClient, err error) {
sac = StorageAuthorityClient{rpc: client}
return
}
@ -1145,10 +1146,10 @@ func (cac StorageAuthorityClient) GetAuthorization(id string) (authz core.Author
}
// GetLatestValidAuthorization sends a request to get an Authorization by RegID, Identifier
func (cac StorageAuthorityClient) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
func (cac StorageAuthorityClient) GetLatestValidAuthorization(registrationID int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
var lvar latestValidAuthorizationRequest
lvar.RegID = registrationId
lvar.RegID = registrationID
lvar.Identifier = identifier
data, err := json.Marshal(lvar)
@ -1369,6 +1370,8 @@ func (cac StorageAuthorityClient) CountCertificatesByNames(names []string, earli
return
}
// GetSCTReceipt retrieves an SCT according to the serial number of a certificate
// and the logID of the log to which it was submitted.
func (cac StorageAuthorityClient) GetSCTReceipt(serial string, logID string) (receipt core.SignedCertificateTimestamp, err error) {
var gsctReq struct {
Serial string
@ -1391,6 +1394,7 @@ func (cac StorageAuthorityClient) GetSCTReceipt(serial string, logID string) (re
return
}
// AddSCTReceipt adds a new SCT to the database.
func (cac StorageAuthorityClient) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) {
data, err := json.Marshal(sct)
if err != nil {

View File

@ -0,0 +1,12 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE `challenges` ADD COLUMN (`keyAuthorization` varchar(255));
ALTER TABLE `challenges` DROP COLUMN `validation`;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
ALTER TABLE `challenges` DROP COLUMN `keyAuthorization`;
ALTER TABLE `challenges` ADD COLUMN (`validation` mediumblob);

View File

@ -11,6 +11,7 @@ import (
"net/url"
"strings"
// Provide access to the MySQL driver
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/go-sql-driver/mysql"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core"

View File

@ -35,6 +35,8 @@ type regModel struct {
}
// challModel is the description of a core.Challenge in the database
//
// The Validation field is a stub; the column is only there for backward compatibility.
type challModel struct {
ID int64 `db:"id"`
AuthorizationID string `db:"authorizationID"`
@ -45,7 +47,7 @@ type challModel struct {
Validated *time.Time `db:"validated"`
Token string `db:"token"`
TLS *bool `db:"tls"`
Validation []byte `db:"validation"`
KeyAuthorization string `db:"keyAuthorization"`
ValidationRecord []byte `db:"validationRecord"`
AccountKey []byte `db:"accountKey"`
@ -99,11 +101,12 @@ func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
Token: c.Token,
TLS: c.TLS,
}
if c.Validation != nil {
cm.Validation = []byte(c.Validation.FullSerialize())
if len(cm.Validation) > mediumBlobSize {
return nil, fmt.Errorf("Validation object is too large to store in the database")
if c.KeyAuthorization != nil {
kaString := c.KeyAuthorization.String()
if len(kaString) > 255 {
return nil, fmt.Errorf("Key authorization is too large to store in the database")
}
cm.KeyAuthorization = kaString
}
if c.Error != nil {
errJSON, err := json.Marshal(c.Error)
@ -147,12 +150,12 @@ func modelToChallenge(cm *challModel) (core.Challenge, error) {
Token: cm.Token,
TLS: cm.TLS,
}
if len(cm.Validation) > 0 {
val, err := jose.ParseSigned(string(cm.Validation))
if len(cm.KeyAuthorization) > 0 {
ka, err := core.NewKeyAuthorizationFromString(cm.KeyAuthorization)
if err != nil {
return core.Challenge{}, err
}
c.Validation = val
c.KeyAuthorization = &ka
}
if len(cm.Error) > 0 {
var problem core.ProblemDetails

View File

@ -212,16 +212,16 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
}
// GetLatestValidAuthorization gets the valid authorization with biggest expire date for a given domain and registrationId
func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationID int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
ident, err := json.Marshal(identifier)
if err != nil {
return
}
var auth core.Authorization
err = ssa.dbMap.SelectOne(&auth, "SELECT id FROM authz "+
"WHERE identifier = :identifier AND registrationID = :registrationId AND status = 'valid' "+
"WHERE identifier = :identifier AND registrationID = :registrationID AND status = 'valid' "+
"ORDER BY expires DESC LIMIT 1",
map[string]interface{}{"identifier": string(ident), "registrationId": registrationId})
map[string]interface{}{"identifier": string(ident), "registrationID": registrationID})
if err != nil {
return
}
@ -229,6 +229,8 @@ func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationId int64
return ssa.GetAuthorization(auth.ID)
}
// TooManyCertificatesError indicates that the number of certificates returned by
// CountCertificates exceeded the hard-coded limit of 10,000 certificates.
type TooManyCertificatesError string
func (t TooManyCertificatesError) Error() string {

View File

@ -168,10 +168,10 @@ func TestAddAuthorization(t *testing.T) {
}
func CreateDomainAuth(t *testing.T, domainName string, sa *SQLStorageAuthority) (authz core.Authorization) {
return CreateDomainAuthWithRegId(t, domainName, sa, 42)
return CreateDomainAuthWithRegID(t, domainName, sa, 42)
}
func CreateDomainAuthWithRegId(t *testing.T, domainName string, sa *SQLStorageAuthority, regID int64) (authz core.Authorization) {
func CreateDomainAuthWithRegID(t *testing.T, domainName string, sa *SQLStorageAuthority, regID int64) (authz core.Authorization) {
// create pending auth
authz, err := sa.NewPendingAuthorization(core.Authorization{RegistrationID: regID, Challenges: []core.Challenge{core.Challenge{}}})
@ -212,7 +212,7 @@ func TestGetLatestValidAuthorizationBasic(t *testing.T) {
reg := satest.CreateWorkingRegistration(t, sa)
// authorize "example.org"
authz = CreateDomainAuthWithRegId(t, "example.org", sa, reg.ID)
authz = CreateDomainAuthWithRegID(t, "example.org", sa, reg.ID)
// finalize auth
authz.Status = core.StatusValid
@ -243,7 +243,7 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
reg := satest.CreateWorkingRegistration(t, sa)
// create invalid authz
authz := CreateDomainAuthWithRegId(t, domain, sa, reg.ID)
authz := CreateDomainAuthWithRegID(t, domain, sa, reg.ID)
exp := time.Now().AddDate(0, 0, 10) // expire in 10 day
authz.Expires = &exp
authz.Status = core.StatusInvalid
@ -255,7 +255,7 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
test.AssertError(t, err, "Should not have found a valid auth for "+domain)
// create valid auth
authz = CreateDomainAuthWithRegId(t, domain, sa, reg.ID)
authz = CreateDomainAuthWithRegID(t, domain, sa, reg.ID)
exp = time.Now().AddDate(0, 0, 1) // expire in 1 day
authz.Expires = &exp
authz.Status = core.StatusValid
@ -271,7 +271,7 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
test.AssertEquals(t, authz.RegistrationID, reg.ID)
// create a newer auth
newAuthz := CreateDomainAuthWithRegId(t, domain, sa, reg.ID)
newAuthz := CreateDomainAuthWithRegID(t, domain, sa, reg.ID)
exp = time.Now().AddDate(0, 0, 2) // expire in 2 day
newAuthz.Expires = &exp
newAuthz.Status = core.StatusValid

24
test.sh
View File

@ -63,13 +63,23 @@ function run() {
}
function run_and_comment() {
if [ "x${TRAVIS}" = "x" ] || [ "${TRAVIS_PULL_REQUEST}" == "false" ] || [ ! -f "${GITHUB_SECRET_FILE}" ] ; then
run "$@"
echo "$@"
result=$("$@" 2>&1)
echo ${result}
if [ "x${result}" == "x" ]; then
update_status --state success
echo "Success: $@"
else
result=$(run "$@")
local status=$?
# Only send a comment if exit code > 0
if [ ${status} -ne 0 ] ; then
FAILURE=1
update_status --state failure
echo "[!] FAILURE: $@"
fi
# If this is a travis PR run, post a comment
if [ "x${TRAVIS}" != "x" ] && [ "${TRAVIS_PULL_REQUEST}" != "false" ] && [ -f "${GITHUB_SECRET_FILE}" ] ; then
# If the output is non-empty, post a comment and mark this as a failure
if [ "x${result}" -ne "x" ] ; then
echo $'```\n'${result}$'\n```' | github-pr-status --authfile $GITHUB_SECRET_FILE \
--owner "letsencrypt" --repo "boulder" \
comment --pr "${TRAVIS_PULL_REQUEST}" -b -
@ -157,7 +167,7 @@ fi
#
if [[ "$RUN" =~ "lint" ]] ; then
start_context "test/golint"
[ -x "$(which golint)" ] && run golint ./...
run_and_comment golint -min_confidence=0.81 ./...
end_context #test/golint
fi

View File

@ -134,9 +134,9 @@
"userAgent": "boulder",
"debugAddr": "localhost:8004",
"portConfig": {
"simpleHTTPPort": 5001,
"simpleHTTPSPort": 5001,
"dvsniPort": 5001
"httpPort": 5001,
"httpsPort": 5001,
"tlsPort": 5001
},
"maxConcurrentRPCServerRequests": 16
},

View File

@ -137,6 +137,12 @@ module.exports = {
return forge.pki.certificateRequestToPem(c);
},
thumbprint: function(publicKey) {
// Only handling RSA keys
input = bytesToBuffer('{"e":"'+ publicKey.e + '","kty":"RSA","n":"'+ publicKey.n +'"}');
return util.b64enc(crypto.createHash('sha256').update(input).digest());
},
///// SIGNATURE GENERATION / VERIFICATION
generateSignature: function(keyPair, payload, nonce) {

View File

@ -20,7 +20,6 @@ var cryptoUtil = require("./crypto-util");
var child_process = require('child_process');
var fs = require('fs');
var http = require('http');
var https = require('https');
var inquirer = require("inquirer");
var request = require('request');
var url = require('url');
@ -103,7 +102,7 @@ var cliOptions = cli.parse({
var state = {
certPrivateKey: null,
accountPrivateKey: null,
accountKeyPair: null,
newRegistrationURL: cliOptions.newReg,
registrationURL: "",
@ -221,8 +220,8 @@ function makeAccountKeyPair(answers) {
console.log(error);
process.exit(1);
}
state.accountPrivateKey = cryptoUtil.importPemPrivateKey(fs.readFileSync("account-key.pem"));
state.acme = new Acme(state.accountPrivateKey);
state.accountKeyPair = cryptoUtil.importPemPrivateKey(fs.readFileSync("account-key.pem"));
state.acme = new Acme(state.accountKeyPair);
console.log();
if (cliOptions.email) {
@ -348,26 +347,20 @@ function getReadyToValidate(err, resp, body) {
var authz = JSON.parse(body);
var simpleHttp = authz.challenges.filter(function(x) { return x.type == "simpleHttp"; });
if (simpleHttp.length == 0) {
var httpChallenges = authz.challenges.filter(function(x) { return x.type == "http-01"; });
if (httpChallenges.length == 0) {
console.log("The server didn't offer any challenges we can handle.");
process.exit(1);
}
var challenge = httpChallenges[0];
// Construct a key authorization for this token and key
var thumbprint = cryptoUtil.thumbprint(state.accountKeyPair.publicKey);
var keyAuthorization = challenge.token + "." + thumbprint;
var challenge = simpleHttp[0];
var path = cryptoUtil.randomString(8) + ".txt";
var challengePath = ".well-known/acme-challenge/" + challenge.token;
state.responseURL = challenge["uri"];
state.path = path;
// Sign validation JWS
var validationObject = JSON.stringify({
type: "simpleHttp",
token: challenge.token,
tls: true
})
var validationJWS = cryptoUtil.generateSignature(state.accountPrivateKey,
new Buffer(validationObject))
state.path = challengePath;
// For local, test-mode validation
function httpResponder(req, response) {
@ -376,18 +369,16 @@ function getReadyToValidate(err, resp, body) {
if ((host.split(/:/)[0] === state.domain || /localhost/.test(state.newRegistrationURL)) &&
req.method === "GET" &&
req.url == "/" + challengePath) {
response.writeHead(200, {"Content-Type": "application/jose+json"});
response.end(JSON.stringify(validationJWS));
console.log("Providing key authorization:", keyAuthorization);
response.writeHead(200, {"Content-Type": "application/json"});
response.end(keyAuthorization);
} else {
console.log("Got invalid request for", req.method, host, req.url);
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("");
}
}
state.httpServer = https.createServer({
cert: fs.readFileSync("temp-cert.pem"),
key: fs.readFileSync(state.keyFile)
}, httpResponder)
state.httpServer = http.createServer(httpResponder)
if (/localhost/.test(state.newRegistrationURL)) {
state.httpServer.listen(5001)
} else {
@ -397,8 +388,7 @@ function getReadyToValidate(err, resp, body) {
cli.spinner("Validating domain");
post(state.responseURL, {
resource: "challenge",
path: state.path,
tls: true,
keyAuthorization: keyAuthorization,
}, ensureValidation);
}

View File

@ -42,24 +42,24 @@ var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups")
// ValidationAuthorityImpl represents a VA
type ValidationAuthorityImpl struct {
RA core.RegistrationAuthority
log *blog.AuditLogger
DNSResolver core.DNSResolver
IssuerDomain string
simpleHTTPPort int
simpleHTTPSPort int
dvsniPort int
UserAgent string
stats statsd.Statter
clk clock.Clock
RA core.RegistrationAuthority
log *blog.AuditLogger
DNSResolver core.DNSResolver
IssuerDomain string
httpPort int
httpsPort int
tlsPort int
UserAgent string
stats statsd.Statter
clk clock.Clock
}
// PortConfig specifies what ports the VA should call to on the remote
// host when performing its checks.
type PortConfig struct {
SimpleHTTPPort int
SimpleHTTPSPort int
DVSNIPort int
HTTPPort int
HTTPSPort int
TLSPort int
}
// NewValidationAuthorityImpl constructs a new VA
@ -67,12 +67,12 @@ func NewValidationAuthorityImpl(pc *PortConfig, stats statsd.Statter, clk clock.
logger := blog.GetAuditLogger()
logger.Notice("Validation Authority Starting")
return &ValidationAuthorityImpl{
log: logger,
simpleHTTPPort: pc.SimpleHTTPPort,
simpleHTTPSPort: pc.SimpleHTTPSPort,
dvsniPort: pc.DVSNIPort,
stats: stats,
clk: clk,
log: logger,
httpPort: pc.HTTPPort,
httpsPort: pc.HTTPSPort,
tlsPort: pc.TLSPort,
stats: stats,
clk: clk,
}
}
@ -86,6 +86,7 @@ type verificationRequestEvent struct {
Error string `json:",omitempty"`
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func verifyValidationJWS(validation *jose.JsonWebSignature, accountKey *jose.JsonWebKey, target map[string]interface{}) error {
if len(validation.Signatures) > 1 {
return fmt.Errorf("Too many signatures on validation JWS")
@ -178,7 +179,7 @@ func (d *dialer) Dial(_, _ string) (net.Conn, error) {
// resolveAndConstructDialer gets the prefered address using va.getAddr and returns
// the chosen address and dialer for that address and correct port.
func (va *ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort string) (dialer, *core.ProblemDetails) {
port := fmt.Sprintf("%d", va.simpleHTTPPort)
port := fmt.Sprintf("%d", va.httpPort)
if defaultPort != "" {
port = defaultPort
}
@ -200,50 +201,37 @@ func (va *ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort s
// Validation methods
func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
func (va *ValidationAuthorityImpl) fetchHTTP(identifier core.AcmeIdentifier, path string, useTLS bool, input core.Challenge) ([]byte, core.Challenge, error) {
emptyBody := []byte{}
challenge := input
if identifier.Type != core.IdentifierDNS {
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem,
Detail: "Identifier type for SimpleHTTP was not DNS",
}
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] Identifier failure", identifier))
return challenge, challenge.Error
}
host := identifier.Value
var scheme string
var port int
if input.TLS == nil || (input.TLS != nil && *input.TLS) {
scheme := "http"
port := va.httpPort
if useTLS {
scheme = "https"
port = va.simpleHTTPSPort
} else {
scheme = "http"
port = va.simpleHTTPPort
port = va.httpsPort
}
portString := fmt.Sprintf("%d", port)
portString := strconv.Itoa(port)
hostPort := net.JoinHostPort(host, portString)
url := &url.URL{
Scheme: scheme,
Host: hostPort,
Path: fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token),
Path: path,
}
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
va.log.Audit(fmt.Sprintf("Attempting to validate Simple%s for %s", strings.ToUpper(scheme), url))
va.log.Audit(fmt.Sprintf("Attempting to validate %s for %s", challenge.Type, url))
httpRequest, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem,
Detail: "URL provided for SimpleHTTP was invalid",
Detail: "URL provided for HTTP was invalid",
}
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] HTTP failure: %s", identifier, err))
va.log.Debug(fmt.Sprintf("%s [%s] HTTP failure: %s", challenge.Type, identifier, err))
challenge.Status = core.StatusInvalid
return challenge, err
return emptyBody, challenge, err
}
if va.UserAgent != "" {
@ -257,7 +245,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
if prob != nil {
challenge.Status = core.StatusInvalid
challenge.Error = prob
return challenge, prob
return emptyBody, challenge, prob
}
tr := &http.Transport{
@ -303,7 +291,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
return err
}
tr.Dial = dialer.Dial
va.log.Info(fmt.Sprintf("validateSimpleHTTP [%s] redirect from %q to %q [%s]", identifier, via[len(via)-1].URL.String(), req.URL.String(), dialer.record.AddressUsed))
va.log.Info(fmt.Sprintf("%s [%s] redirect from %q to %q [%s]", challenge.Type, identifier, via[len(via)-1].URL.String(), req.URL.String(), dialer.record.AddressUsed))
return nil
}
client := http.Client{
@ -319,7 +307,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
Detail: fmt.Sprintf("Could not connect to %s", url),
}
va.log.Debug(strings.Join([]string{challenge.Error.Error(), err.Error()}, ": "))
return challenge, err
return emptyBody, challenge, err
}
if httpResponse.StatusCode != 200 {
@ -330,18 +318,107 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
url.String(), dialer.record.AddressUsed, httpResponse.StatusCode),
}
err = challenge.Error
return challenge, err
return emptyBody, challenge, err
}
// Read body & test
body, readErr := ioutil.ReadAll(httpResponse.Body)
if readErr != nil {
body, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: fmt.Sprintf("Error reading HTTP response body"),
Detail: fmt.Sprintf("Error reading HTTP response body: %v", err),
}
return challenge, readErr
return emptyBody, challenge, err
}
return body, challenge, nil
}
func (va *ValidationAuthorityImpl) validateTLSWithZName(identifier core.AcmeIdentifier, input core.Challenge, zName string) (core.Challenge, error) {
challenge := input
addr, allAddrs, problem := va.getAddr(identifier.Value)
challenge.ValidationRecord = []core.ValidationRecord{
core.ValidationRecord{
Hostname: identifier.Value,
AddressesResolved: allAddrs,
AddressUsed: addr,
},
}
if problem != nil {
challenge.Status = core.StatusInvalid
challenge.Error = problem
return challenge, challenge.Error
}
// Make a connection with SNI = nonceName
portString := strconv.Itoa(va.tlsPort)
hostPort := net.JoinHostPort(addr.String(), portString)
challenge.ValidationRecord[0].Port = portString
va.log.Notice(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", challenge.Type, identifier, hostPort, zName))
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: validationTimeout}, "tcp", hostPort, &tls.Config{
ServerName: zName,
InsecureSkipVerify: true,
})
if err != nil {
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: parseHTTPConnError(err),
Detail: "Failed to connect to host for DVSNI challenge",
}
va.log.Debug(fmt.Sprintf("%s [%s] TLS Connection failure: %s", challenge.Type, identifier, err))
return challenge, err
}
defer conn.Close()
// Check that zName is a dNSName SAN in the server's certificate
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: "No certs presented for TLS SNI challenge",
}
challenge.Status = core.StatusInvalid
return challenge, challenge.Error
}
for _, name := range certs[0].DNSNames {
if subtle.ConstantTimeCompare([]byte(name), []byte(zName)) == 1 {
challenge.Status = core.StatusValid
return challenge, nil
}
}
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: "Correct zName not found for TLS SNI challenge",
}
challenge.Status = core.StatusInvalid
return challenge, challenge.Error
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input
if identifier.Type != core.IdentifierDNS {
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem,
Detail: "Identifier type for SimpleHTTP was not DNS",
}
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] Identifier failure", identifier))
return challenge, challenge.Error
}
// Perform the fetch
path := fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token)
useTLS := (challenge.TLS == nil) || *challenge.TLS
body, challenge, err := va.fetchHTTP(identifier, path, useTLS, challenge)
if err != nil {
return challenge, err
}
// Parse and verify JWS
@ -381,6 +458,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
return challenge, nil
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
func (va *ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input
@ -417,67 +495,82 @@ func (va *ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier,
h := sha256.New()
h.Write([]byte(encodedSignature))
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.DVSNISuffix)
ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.TLSSNISuffix)
addr, allAddrs, problem := va.getAddr(identifier.Value)
challenge.ValidationRecord = []core.ValidationRecord{
core.ValidationRecord{
Hostname: identifier.Value,
AddressesResolved: allAddrs,
AddressUsed: addr,
},
}
if problem != nil {
return va.validateTLSWithZName(identifier, challenge, ZName)
}
func (va *ValidationAuthorityImpl) validateHTTP01(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input
if identifier.Type != core.IdentifierDNS {
challenge.Status = core.StatusInvalid
challenge.Error = problem
challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem,
Detail: "Identifier type for HTTP validation was not DNS",
}
va.log.Debug(fmt.Sprintf("%s [%s] Identifier failure", challenge.Type, identifier))
return challenge, challenge.Error
}
// Make a connection with SNI = nonceName
portString := fmt.Sprintf("%d", va.dvsniPort)
hostPort := net.JoinHostPort(addr.String(), portString)
challenge.ValidationRecord[0].Port = portString
va.log.Notice(fmt.Sprintf("DVSNI [%s] Attempting to validate DVSNI for %s %s",
identifier, hostPort, ZName))
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: validationTimeout}, "tcp", hostPort, &tls.Config{
ServerName: ZName,
InsecureSkipVerify: true,
})
// Perform the fetch
path := fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token)
body, challenge, err := va.fetchHTTP(identifier, path, false, challenge)
if err != nil {
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: parseHTTPConnError(err),
Detail: "Failed to connect to host for DVSNI challenge",
}
va.log.Debug(fmt.Sprintf("DVSNI [%s] TLS Connection failure: %s", identifier, err))
return challenge, err
}
defer conn.Close()
// Check that ZName is a dNSName SAN in the server's certificate
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
// Parse body as a key authorization object
serverKeyAuthorization, err := core.NewKeyAuthorizationFromString(string(body))
if err != nil {
err = fmt.Errorf("Error parsing key authorization file: %s", err.Error())
va.log.Debug(err.Error())
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: "No certs presented for DVSNI challenge",
}
challenge.Status = core.StatusInvalid
return challenge, challenge.Error
}
for _, name := range certs[0].DNSNames {
if subtle.ConstantTimeCompare([]byte(name), []byte(ZName)) == 1 {
challenge.Status = core.StatusValid
return challenge, nil
Detail: err.Error(),
}
return challenge, err
}
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: "Correct ZName not found for DVSNI challenge",
// Check that the account key for this challenge is authorized by this object
if !serverKeyAuthorization.Match(challenge.Token, challenge.AccountKey) {
err = fmt.Errorf("The key authorization file from the server did not match this challenge [%v] != [%v]",
challenge.KeyAuthorization.String(), string(body))
va.log.Debug(err.Error())
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: err.Error(),
}
return challenge, err
}
challenge.Status = core.StatusInvalid
return challenge, challenge.Error
challenge.Status = core.StatusValid
return challenge, nil
}
func (va *ValidationAuthorityImpl) validateTLSSNI01(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input
if identifier.Type != "dns" {
challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem,
Detail: "Identifier type for TLS-SNI was not DNS",
}
challenge.Status = core.StatusInvalid
va.log.Debug(fmt.Sprintf("TLS-SNI [%s] Identifier failure", identifier))
return challenge, challenge.Error
}
// Compute the digest that will appear in the certificate
h := sha256.New()
h.Write([]byte(challenge.KeyAuthorization.String()))
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.TLSSNISuffix)
return va.validateTLSWithZName(identifier, challenge, ZName)
}
// parseHTTPConnError returns the ACME ProblemType corresponding to an error
@ -502,7 +595,7 @@ func parseHTTPConnError(err error) core.ProblemType {
return core.ConnectionProblem
}
func (va *ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
func (va *ValidationAuthorityImpl) validateDNS01(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input
if identifier.Type != core.IdentifierDNS {
@ -515,24 +608,10 @@ func (va *ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, i
return challenge, challenge.Error
}
// Check that JWS body is as expected
// * "type" == "dvsni"
// * "token" == challenge.token
target := map[string]interface{}{
"type": core.ChallengeTypeDNS,
"token": challenge.Token,
}
err := verifyValidationJWS(challenge.Validation, challenge.AccountKey, target)
if err != nil {
va.log.Debug(err.Error())
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: err.Error(),
}
return challenge, err
}
encodedSignature := core.B64enc(challenge.Validation.Signatures[0].Signature)
// Compute the digest of the key authorization file
h := sha256.New()
h.Write([]byte(challenge.KeyAuthorization.String()))
authorizedKeysDigest := hex.EncodeToString(h.Sum(nil))
// Look for the required record in the DNS
challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, identifier.Value)
@ -548,7 +627,7 @@ func (va *ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, i
}
for _, element := range txts {
if subtle.ConstantTimeCompare([]byte(element), []byte(encodedSignature)) == 1 {
if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 {
challenge.Status = core.StatusValid
return challenge, nil
}
@ -583,11 +662,17 @@ func (va *ValidationAuthorityImpl) validate(authz core.Authorization, challengeI
vStart := va.clk.Now()
switch authz.Challenges[challengeIndex].Type {
case core.ChallengeTypeSimpleHTTP:
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this case
authz.Challenges[challengeIndex], err = va.validateSimpleHTTP(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeDVSNI:
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this case
authz.Challenges[challengeIndex], err = va.validateDvsni(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeDNS:
authz.Challenges[challengeIndex], err = va.validateDNS(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeHTTP01:
authz.Challenges[challengeIndex], err = va.validateHTTP01(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeTLSSNI01:
authz.Challenges[challengeIndex], err = va.validateTLSSNI01(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeDNS01:
authz.Challenges[challengeIndex], err = va.validateDNS01(authz.Identifier, authz.Challenges[challengeIndex])
}
va.stats.TimingDuration(fmt.Sprintf("VA.Validations.%s.%s", authz.Challenges[challengeIndex].Type, authz.Challenges[challengeIndex].Status), time.Since(vStart), 1.0)

View File

@ -65,15 +65,21 @@ var ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "localhost"}
var log = mocks.UseMockLog()
const expectedToken = "THETOKEN"
const pathWrongToken = "wrongtoken"
// All paths that get assigned to tokens MUST be valid tokens
const expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
const pathWrongToken = "i6lNAC4lOOLYCl-A08VJt9z_tKYvVk63Dumo8icsBjQ"
const path404 = "404"
const pathFound = "302"
const pathMoved = "301"
const pathRedirectLookup = "re-lookup"
const pathRedirectLookupInvalid = "re-lookup-invalid"
const pathFound = "GBq8SwWq3JsbREFdCamk5IX3KLsxW5ULeGs98Ajl_UM"
const pathMoved = "5J4FIMrWNfmvHZo-QpKZngmuhqZGwRm21-oEgUDstJM"
const pathRedirectPort = "port-redirect"
const pathWait = "wait"
const pathWaitLong = "wait-long"
const pathReLookup = "7e-P57coLM7D3woNTp_xbJrtlkDYy6PWf3mSSbLwCr4"
const pathReLookupInvalid = "re-lookup-invalid"
const pathLooper = "looper"
const pathValid = "valid"
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func createValidation(token string, enableTLS bool) string {
payload, _ := json.Marshal(map[string]interface{}{
"type": "simpleHttp",
@ -85,6 +91,7 @@ func createValidation(token string, enableTLS bool) string {
return obj.FullSerialize()
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
m := http.NewServeMux()
@ -110,22 +117,22 @@ func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
currentToken = pathFound
}
http.Redirect(w, r, pathMoved, 302)
} else if strings.HasSuffix(r.URL.Path, "wait") {
} else if strings.HasSuffix(r.URL.Path, pathWait) {
t.Logf("SIMPLESRV: Got a wait req\n")
time.Sleep(time.Second * 3)
} else if strings.HasSuffix(r.URL.Path, "wait-long") {
} else if strings.HasSuffix(r.URL.Path, pathWaitLong) {
t.Logf("SIMPLESRV: Got a wait-long req\n")
time.Sleep(time.Second * 10)
} else if strings.HasSuffix(r.URL.Path, "re-lookup") {
} else if strings.HasSuffix(r.URL.Path, pathReLookup) {
t.Logf("SIMPLESRV: Got a redirect req to a valid hostname\n")
if currentToken == defaultToken {
currentToken = "re-lookup"
currentToken = pathReLookup
}
http.Redirect(w, r, "http://other.valid/path", 302)
} else if strings.HasSuffix(r.URL.Path, "re-lookup-invalid") {
} else if strings.HasSuffix(r.URL.Path, pathReLookupInvalid) {
t.Logf("SIMPLESRV: Got a redirect req to a invalid hostname\n")
http.Redirect(w, r, "http://invalid.invalid/path", 302)
} else if strings.HasSuffix(r.URL.Path, "looper") {
} else if strings.HasSuffix(r.URL.Path, pathLooper) {
t.Logf("SIMPLESRV: Got a loop req\n")
http.Redirect(w, r, r.URL.String(), 301)
} else if strings.HasSuffix(r.URL.Path, pathRedirectPort) {
@ -174,6 +181,7 @@ func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
return server
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature)
h := sha256.New()
@ -221,17 +229,7 @@ func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
return hs
}
func brokenTLSSrv() *httptest.Server {
server := httptest.NewUnstartedServer(http.DefaultServeMux)
server.TLS = &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, fmt.Errorf("Failing on purpose")
},
}
server.StartTLS()
return server
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func TestSimpleHttpTLS(t *testing.T) {
chall := core.Challenge{
Type: core.ChallengeTypeSimpleHTTP,
@ -246,18 +244,19 @@ func TestSimpleHttpTLS(t *testing.T) {
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{HTTPSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Error validating simpleHttp")
logs := log.GetAllMatching(`^\[AUDIT\] Attempting to validate SimpleHTTPS for `)
logs := log.GetAllMatching(`^\[AUDIT\] Attempting to validate simpleHttp for `)
test.AssertEquals(t, len(logs), 1)
test.AssertEquals(t, logs[0].Priority, syslog.LOG_NOTICE)
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func TestSimpleHttp(t *testing.T) {
tls := false
chall := core.Challenge{
@ -288,16 +287,16 @@ func TestSimpleHttp(t *testing.T) {
badPort = goodPort - 1
}
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: badPort}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: badPort}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
invalidChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
va = NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: goodPort}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va = NewValidationAuthorityImpl(&PortConfig{HTTPPort: goodPort}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
@ -327,15 +326,15 @@ func TestSimpleHttp(t *testing.T) {
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Failed to follow 301 redirect")
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
log.Clear()
chall.Token = pathFound
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Failed to follow 302 redirect")
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/302" to ".*/301"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
ipIdentifier := core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}
invalidChall, err = va.validateSimpleHTTP(ipIdentifier, chall)
@ -360,6 +359,7 @@ func TestSimpleHttp(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func TestSimpleHttpRedirectLookup(t *testing.T) {
tls := false
chall := core.Challenge{
@ -374,15 +374,15 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
chall.Token = pathMoved
finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 2)
log.Clear()
@ -390,12 +390,12 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/302" to ".*/301"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 3)
log.Clear()
chall.Token = pathRedirectLookupInvalid
chall.Token = pathReLookupInvalid
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
@ -403,11 +403,11 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
test.AssertEquals(t, len(log.GetAllMatching(`No IPv4 addresses found for invalid.invalid`)), 1)
log.Clear()
chall.Token = pathRedirectLookup
chall.Token = pathReLookup
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/re-lookup" to ".*other.valid/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathReLookup+`" to ".*other.valid/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
@ -422,6 +422,7 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func TestSimpleHttpRedirectLoop(t *testing.T) {
tls := false
chall := core.Challenge{
@ -435,8 +436,8 @@ func TestSimpleHttpRedirectLoop(t *testing.T) {
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall)
@ -445,22 +446,7 @@ func TestSimpleHttpRedirectLoop(t *testing.T) {
fmt.Println(finChall)
}
func getPort(hs *httptest.Server) (int, error) {
url, err := url.Parse(hs.URL)
if err != nil {
return 0, err
}
_, portString, err := net.SplitHostPort(url.Host)
if err != nil {
return 0, err
}
port, err := strconv.ParseInt(portString, 10, 64)
if err != nil {
return 0, err
}
return int(port), nil
}
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func TestDvsni(t *testing.T) {
chall := createChallenge(core.ChallengeTypeDVSNI)
@ -469,9 +455,9 @@ func TestDvsni(t *testing.T) {
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{DVSNIPort: port}, stats, clock.Default())
va := NewValidationAuthorityImpl(&PortConfig{TLSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
finChall, err := va.validateDvsni(ident, chall)
@ -523,15 +509,15 @@ func TestDvsni(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
}
func TestTLSError(t *testing.T) {
func TestDVSNIWithTLSError(t *testing.T) {
chall := createChallenge(core.ChallengeTypeDVSNI)
hs := brokenTLSSrv()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{DVSNIPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{TLSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
invalidChall, err := va.validateDvsni(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
@ -539,19 +525,403 @@ func TestTLSError(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.TLSProblem)
}
func TestValidateHTTP(t *testing.T) {
tls := false
challHTTP := core.SimpleHTTPChallenge()
challHTTP.TLS = &tls
challHTTP.ValidationRecord = []core.ValidationRecord{}
challHTTP.AccountKey = accountKey
func httpSrv(t *testing.T, token string) *httptest.Server {
m := http.NewServeMux()
hs := simpleSrv(t, challHTTP.Token, tls)
defaultToken := token
currentToken := defaultToken
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.Host, "localhost:") && r.Host != "other.valid" && r.Host != "other.valid:8080" {
t.Errorf("Bad Host header: " + r.Host)
}
if strings.HasSuffix(r.URL.Path, path404) {
t.Logf("HTTPSRV: Got a 404 req\n")
http.NotFound(w, r)
} else if strings.HasSuffix(r.URL.Path, pathMoved) {
t.Logf("HTTPSRV: Got a 301 redirect req\n")
if currentToken == defaultToken {
currentToken = pathMoved
}
http.Redirect(w, r, pathValid, 301)
} else if strings.HasSuffix(r.URL.Path, pathFound) {
t.Logf("HTTPSRV: Got a 302 redirect req\n")
if currentToken == defaultToken {
currentToken = pathFound
}
http.Redirect(w, r, pathMoved, 302)
} else if strings.HasSuffix(r.URL.Path, pathWait) {
t.Logf("HTTPSRV: Got a wait req\n")
time.Sleep(time.Second * 3)
} else if strings.HasSuffix(r.URL.Path, pathWaitLong) {
t.Logf("HTTPSRV: Got a wait-long req\n")
time.Sleep(time.Second * 10)
} else if strings.HasSuffix(r.URL.Path, pathReLookup) {
t.Logf("HTTPSRV: Got a redirect req to a valid hostname\n")
if currentToken == defaultToken {
currentToken = pathReLookup
}
http.Redirect(w, r, "http://other.valid/path", 302)
} else if strings.HasSuffix(r.URL.Path, pathReLookupInvalid) {
t.Logf("HTTPSRV: Got a redirect req to a invalid hostname\n")
http.Redirect(w, r, "http://invalid.invalid/path", 302)
} else if strings.HasSuffix(r.URL.Path, pathLooper) {
t.Logf("HTTPSRV: Got a loop req\n")
http.Redirect(w, r, r.URL.String(), 301)
} else if strings.HasSuffix(r.URL.Path, pathRedirectPort) {
t.Logf("HTTPSRV: Got a port redirect req\n")
http.Redirect(w, r, "http://other.valid:8080/path", 302)
} else {
t.Logf("HTTPSRV: Got a valid req\n")
t.Logf("HTTPSRV: Path = %s\n", r.URL.Path)
keyAuthz, _ := core.NewKeyAuthorization(currentToken, accountKey)
t.Logf("HTTPSRV: Key Authz = %s\n", keyAuthz.String())
fmt.Fprint(w, keyAuthz.String())
currentToken = defaultToken
}
})
server := httptest.NewUnstartedServer(m)
server.Start()
return server
}
func tlssniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
h := sha256.New()
h.Write([]byte(chall.KeyAuthorization.String()))
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:])
template := &x509.Certificate{
SerialNumber: big.NewInt(1337),
Subject: pkix.Name{
Organization: []string{"tests"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, 1),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{ZName},
}
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
cert := &tls.Certificate{
Certificate: [][]byte{certBytes},
PrivateKey: &TheKey,
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*cert},
ClientAuth: tls.NoClientCert,
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if clientHello.ServerName != ZName {
time.Sleep(time.Second * 10)
return nil, nil
}
return cert, nil
},
NextProtos: []string{"http/1.1"},
}
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
hs.TLS = tlsConfig
hs.StartTLS()
return hs
}
func TestHttp(t *testing.T) {
chall := core.HTTPChallenge01(accountKey)
err := setChallengeToken(&chall, expectedToken)
test.AssertNotError(t, err, "Failed to complete HTTP challenge")
// NOTE: We do not attempt to shut down the server. The problem is that the
// "wait-long" handler sleeps for ten seconds, but this test finishes in less
// than that. So if we try to call hs.Close() at the end of the test, we'll be
// closing the test server while a request is still pending. Unfortunately,
// there appears to be an issue in httptest that trips Go's race detector when
// that happens, failing the test. So instead, we live with leaving the server
// around till the process exits.
// TODO(#661): add hs.Close back, see ticket for blocker
hs := httpSrv(t, chall.Token)
goodPort, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
// Attempt to fail a challenge by telling the VA to connect to a port we are
// not listening on.
badPort := goodPort + 1
if badPort == 65536 {
badPort = goodPort - 1
}
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: badPort}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
invalidChall, err := va.validateHTTP01(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
va = NewValidationAuthorityImpl(&PortConfig{HTTPPort: goodPort}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
t.Logf("Trying to validate: %+v\n", chall)
finChall, err := va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Error validating http")
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
log.Clear()
setChallengeToken(&chall, path404)
invalidChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Should have found a 404 for the challenge.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
log.Clear()
setChallengeToken(&chall, pathWrongToken)
// The "wrong token" will actually be the expectedToken. It's wrong
// because it doesn't match pathWrongToken.
invalidChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Should have found the wrong token value.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
log.Clear()
setChallengeToken(&chall, pathMoved)
finChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Failed to follow 301 redirect")
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
log.Clear()
setChallengeToken(&chall, pathFound)
finChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Failed to follow 302 redirect")
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
ipIdentifier := core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}
invalidChall, err = va.validateHTTP01(ipIdentifier, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
invalidChall, err = va.validateHTTP01(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Domain name is invalid.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
setChallengeToken(&chall, pathWaitLong)
started := time.Now()
invalidChall, err = va.validateHTTP01(ident, chall)
took := time.Since(started)
// Check that the HTTP connection times out after 5 seconds and doesn't block for 10 seconds
test.Assert(t, (took > (time.Second * 5)), "HTTP timed out before 5 seconds")
test.Assert(t, (took < (time.Second * 10)), "HTTP connection didn't timeout after 5 seconds")
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Connection should've timed out")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
}
func TestHTTPRedirectLookup(t *testing.T) {
chall := core.HTTPChallenge01(accountKey)
err := setChallengeToken(&chall, expectedToken)
test.AssertNotError(t, err, "Failed to complete HTTP challenge")
hs := httpSrv(t, expectedToken)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
setChallengeToken(&chall, pathMoved)
finChall, err := va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 2)
log.Clear()
setChallengeToken(&chall, pathFound)
finChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 3)
log.Clear()
setChallengeToken(&chall, pathReLookupInvalid)
finChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`No IPv4 addresses found for invalid.invalid`)), 1)
log.Clear()
setChallengeToken(&chall, pathReLookup)
finChall, err = va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathReLookup+`" to ".*other.valid/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
log.Clear()
setChallengeToken(&chall, pathRedirectPort)
finChall, err = va.validateHTTP01(ident, chall)
fmt.Println(finChall.ValidationRecord)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/port-redirect" to ".*other.valid:8080/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
}
func TestHTTPRedirectLoop(t *testing.T) {
chall := core.HTTPChallenge01(accountKey)
err := setChallengeToken(&chall, "looper")
test.AssertNotError(t, err, "Failed to complete HTTP challenge")
hs := httpSrv(t, expectedToken)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
finChall, err := va.validateHTTP01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
fmt.Println(finChall)
}
func getPort(hs *httptest.Server) (int, error) {
url, err := url.Parse(hs.URL)
if err != nil {
return 0, err
}
_, portString, err := net.SplitHostPort(url.Host)
if err != nil {
return 0, err
}
port, err := strconv.ParseInt(portString, 10, 64)
if err != nil {
return 0, err
}
return int(port), nil
}
func TestTLSSNI(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{TLSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
log.Clear()
finChall, err := va.validateTLSSNI01(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "")
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
log.Clear()
invalidChall, err := va.validateTLSSNI01(core.AcmeIdentifier{
Type: core.IdentifierType("ip"),
Value: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port)),
}, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
log.Clear()
invalidChall, err = va.validateTLSSNI01(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Domain name was supposed to be invalid.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
// Need to create a new authorized keys object to get an unknown SNI (from the signature value)
chall.Token = core.NewToken()
keyAuthorization, _ := core.NewKeyAuthorization(chall.Token, accountKey)
chall.KeyAuthorization = &keyAuthorization
log.Clear()
started := time.Now()
invalidChall, err = va.validateTLSSNI01(ident, chall)
took := time.Since(started)
// Check that the HTTP connection times out after 5 seconds and doesn't block for 10 seconds
test.Assert(t, (took > (time.Second * 5)), "HTTP timed out before 5 seconds")
test.Assert(t, (took < (time.Second * 10)), "HTTP connection didn't timeout after 5 seconds")
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Connection should've timed out")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
// Take down validation server and check that validation fails.
hs.Close()
invalidChall, err = va.validateTLSSNI01(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
}
func brokenTLSSrv() *httptest.Server {
server := httptest.NewUnstartedServer(http.DefaultServeMux)
server.TLS = &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, fmt.Errorf("Failing on purpose")
},
}
server.StartTLS()
return server
}
func TestTLSError(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := brokenTLSSrv()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{TLSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
invalidChall, err := va.validateTLSSNI01(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "What cert was used?")
test.AssertEquals(t, invalidChall.Error.Type, core.TLSProblem)
}
func TestValidateHTTP(t *testing.T) {
chall := core.HTTPChallenge01(accountKey)
err := setChallengeToken(&chall, core.NewToken())
test.AssertNotError(t, err, "Failed to complete HTTP challenge")
hs := httpSrv(t, chall.Token)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -561,14 +931,14 @@ func TestValidateHTTP(t *testing.T) {
ID: core.NewToken(),
RegistrationID: 1,
Identifier: ident,
Challenges: []core.Challenge{challHTTP},
Challenges: []core.Challenge{chall},
}
va.validate(authz, 0)
test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status)
}
// challengeType == "dvsni" or "dns", since they're the same
// challengeType == "tls-sni-00" or "dns-00", since they're the same
func createChallenge(challengeType string) core.Challenge {
chall := core.Challenge{
Type: challengeType,
@ -578,27 +948,45 @@ func createChallenge(challengeType string) core.Challenge {
AccountKey: accountKey,
}
keyAuthorization, _ := core.NewKeyAuthorization(chall.Token, accountKey)
chall.KeyAuthorization = &keyAuthorization
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this block
validationPayload, _ := json.Marshal(map[string]interface{}{
"type": chall.Type,
"token": chall.Token,
})
signer, _ := jose.NewSigner(jose.RS256, &TheKey)
chall.Validation, _ = signer.Sign(validationPayload, "")
return chall
}
func TestValidateDvsni(t *testing.T) {
chall := createChallenge(core.ChallengeTypeDVSNI)
hs := dvsniSrv(t, chall)
// setChallengeToken sets the token value both in the Token field and
// in the serialized KeyAuthorization object.
func setChallengeToken(ch *core.Challenge, token string) (err error) {
ch.Token = token
keyAuthorization, err := core.NewKeyAuthorization(token, ch.AccountKey)
if err != nil {
return
}
ch.KeyAuthorization = &keyAuthorization
return
}
func TestValidateTLSSNI01(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{DVSNIPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va := NewValidationAuthorityImpl(&PortConfig{TLSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -613,14 +1001,14 @@ func TestValidateDvsni(t *testing.T) {
test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status)
}
func TestValidateDvsniNotSane(t *testing.T) {
func TestValidateTLSSNINotSane(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) // no calls made
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chall := createChallenge(core.ChallengeTypeDVSNI)
chall := createChallenge(core.ChallengeTypeTLSSNI01)
chall.Token = "not sane"
@ -638,20 +1026,20 @@ func TestValidateDvsniNotSane(t *testing.T) {
func TestUpdateValidations(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
tls := false
challHTTP := core.SimpleHTTPChallenge()
challHTTP.TLS = &tls
challHTTP.ValidationRecord = []core.ValidationRecord{}
chall := core.HTTPChallenge01(accountKey)
chall.ValidationRecord = []core.ValidationRecord{}
err := setChallengeToken(&chall, core.NewToken())
test.AssertNotError(t, err, "Failed to complete HTTP challenge")
var authz = core.Authorization{
ID: core.NewToken(),
RegistrationID: 1,
Identifier: ident,
Challenges: []core.Challenge{challHTTP},
Challenges: []core.Challenge{chall},
}
started := time.Now()
@ -694,7 +1082,7 @@ func TestCAAChecking(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
va.IssuerDomain = "letsencrypt.org"
for _, caaTest := range tests {
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain})
@ -727,11 +1115,11 @@ func TestCAAChecking(t *testing.T) {
func TestDNSValidationFailure(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chalDNS := createChallenge(core.ChallengeTypeDNS)
chalDNS := createChallenge(core.ChallengeTypeDNS01)
var authz = core.Authorization{
ID: core.NewToken(),
@ -753,7 +1141,7 @@ func TestDNSValidationInvalid(t *testing.T) {
Value: "790DB180-A274-47A4-855F-31C428CB1072",
}
chalDNS := core.DNSChallenge()
chalDNS := core.DNSChallenge01(accountKey)
var authz = core.Authorization{
ID: core.NewToken(),
@ -764,7 +1152,7 @@ func TestDNSValidationInvalid(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -778,17 +1166,17 @@ func TestDNSValidationInvalid(t *testing.T) {
func TestDNSValidationNotSane(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chal0 := core.DNSChallenge()
chal0 := core.DNSChallenge01(accountKey)
chal0.Token = ""
chal1 := core.DNSChallenge()
chal1 := core.DNSChallenge01(accountKey)
chal1.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_"
chal2 := core.DNSChallenge()
chal2 := core.DNSChallenge01(accountKey)
chal2.TLS = new(bool)
*chal2.TLS = true
@ -809,11 +1197,11 @@ func TestDNSValidationNotSane(t *testing.T) {
func TestDNSValidationServFail(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chalDNS := createChallenge(core.ChallengeTypeDNS)
chalDNS := createChallenge(core.ChallengeTypeDNS01)
badIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS,
@ -839,7 +1227,7 @@ func TestDNSValidationNoServer(t *testing.T) {
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chalDNS := createChallenge(core.ChallengeTypeDNS)
chalDNS := createChallenge(core.ChallengeTypeDNS01)
var authz = core.Authorization{
ID: core.NewToken(),
@ -860,11 +1248,11 @@ func TestDNSValidationNoServer(t *testing.T) {
func TestDNSValidationLive(t *testing.T) {
stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{}
va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
goodChalDNS := core.DNSChallenge()
goodChalDNS := core.DNSChallenge01(accountKey)
// This token is set at _acme-challenge.good.bin.coffee
goodChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w"
@ -891,7 +1279,7 @@ func TestDNSValidationLive(t *testing.T) {
t.Logf("TestDNSValidationLive on Good did not succeed.")
}
badChalDNS := core.DNSChallenge()
badChalDNS := core.DNSChallenge01(accountKey)
// This token is NOT set at _acme-challenge.bad.bin.coffee
badChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w"

View File

@ -38,10 +38,14 @@ const (
IssuerPath = "/acme/issuer-cert"
BuildIDPath = "/build"
// Not included in net/http
// StatusRateLimited is not in net/http
StatusRateLimited = 429
)
// WebFrontEndImpl provides all the logic for Boulder's web-facing interface,
// i.e., ACME. Its members configure the paths for various ACME functions,
// plus a few other data items used in ACME. Its methods are primarily handlers
// for HTTPS requests for the various ACME functions.
type WebFrontEndImpl struct {
RA core.RegistrationAuthority
SA core.StorageGetter
@ -113,7 +117,7 @@ func statusCodeFromError(err interface{}) int {
type requestEvent struct {
ID string `json:",omitempty"`
RealIP string `json:",omitempty"`
ForwardedFor string `json:",omitempty"`
ClientAddr string `json:",omitempty"`
Endpoint string `json:",omitempty"`
Method string `json:",omitempty"`
RequestTime time.Time `json:",omitempty"`
@ -302,6 +306,8 @@ func addCacheHeader(w http.ResponseWriter, age float64) {
w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%.f", age))
}
// Directory is an HTTP request handler that simply provides the directory
// object stored in the WFE's DirectoryJSON member.
func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Type", "application/json")
response.Write(wfe.DirectoryJSON)
@ -553,8 +559,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques
// Use an explicitly typed variable. Otherwise `go vet' incorrectly complains
// that reg.ID is a string being passed to %d.
var id int64 = reg.ID
regURL := fmt.Sprintf("%s%d", wfe.RegBase, id)
regURL := fmt.Sprintf("%s%d", wfe.RegBase, reg.ID)
responseBody, err := json.Marshal(reg)
if err != nil {
logEvent.Error = err.Error()
@ -730,13 +735,13 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
}
}
func (wfe *WebFrontEndImpl) logCsr(remoteAddr string, cr core.CertificateRequest, registration core.Registration) {
func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateRequest, registration core.Registration) {
var csrLog = struct {
RemoteAddr string
ClientAddr string
CsrBase64 []byte
Registration core.Registration
}{
RemoteAddr: remoteAddr,
ClientAddr: getClientAddr(request),
CsrBase64: cr.Bytes,
Registration: registration,
}
@ -778,7 +783,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
wfe.sendError(response, "Error unmarshaling certificate request", err, http.StatusBadRequest)
return
}
wfe.logCsr(request.RemoteAddr, certificateRequest, reg)
wfe.logCsr(request, certificateRequest, reg)
// Check that the key in the CSR is good. This will also be checked in the CA
// component, but we want to discard CSRs with bad keys as early as possible
// because (a) it's an easy check and we can save unnecessary requests and
@ -833,6 +838,8 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
}
}
// Challenge handles POST requests to challenge URLs. Such requests are clients'
// responses to the server's challenges.
func (wfe *WebFrontEndImpl) Challenge(
response http.ResponseWriter,
request *http.Request) {
@ -1298,15 +1305,26 @@ func (wfe *WebFrontEndImpl) logRequestDetails(logEvent *requestEvent) {
func (wfe *WebFrontEndImpl) populateRequestEvent(request *http.Request) (logEvent requestEvent) {
logEvent = requestEvent{
ID: core.NewToken(),
RealIP: request.Header.Get("X-Real-IP"),
ForwardedFor: request.Header.Get("X-Forwarded-For"),
Method: request.Method,
RequestTime: time.Now(),
Extra: make(map[string]interface{}, 0),
ID: core.NewToken(),
RealIP: request.Header.Get("X-Real-IP"),
ClientAddr: getClientAddr(request),
Method: request.Method,
RequestTime: time.Now(),
Extra: make(map[string]interface{}, 0),
}
if request.URL != nil {
logEvent.Endpoint = request.URL.String()
}
return
}
// Comma-separated list of HTTP clients involved in making this
// request, starting with the original requestor and ending with the
// remote end of our TCP connection (which is typically our own
// proxy).
func getClientAddr(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return xff + "," + r.RemoteAddr
}
return r.RemoteAddr
}

View File

@ -26,10 +26,9 @@ 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/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/cmd"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/ra"
@ -172,7 +171,7 @@ func (ca *MockCA) RevokeCertificate(serial string, reasonCode core.RevocationCod
type MockPA struct{}
func (pa *MockPA) ChallengesFor(identifier core.AcmeIdentifier) (challenges []core.Challenge, combinations [][]int) {
func (pa *MockPA) ChallengesFor(identifier core.AcmeIdentifier, key *jose.JsonWebKey) (challenges []core.Challenge, combinations [][]int, err error) {
return
}
@ -214,7 +213,7 @@ func setupWFE(t *testing.T) WebFrontEndImpl {
wfe.log.SyslogWriter = mocks.NewSyslogWriter()
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL
@ -534,7 +533,7 @@ func TestIssueCertificate(t *testing.T) {
wfe := setupWFE(t)
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
mockLog := wfe.log.SyslogWriter.(*mocks.MockSyslogWriter)
mockLog := wfe.log.SyslogWriter.(*mocks.SyslogWriter)
// The mock CA we use always returns the same test certificate, with a Not
// Before of 2015-09-22. Since we're currently using a real RA instead of a
@ -549,10 +548,10 @@ func TestIssueCertificate(t *testing.T) {
// authorized, etc.
stats, _ := statsd.NewNoopClient(nil)
ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log, stats, cmd.RateLimitConfig{})
ra.SA = &mocks.MockSA{}
ra.SA = &mocks.StorageAuthority{}
ra.CA = &MockCA{}
ra.PA = &MockPA{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
wfe.RA = &ra
responseWriter := httptest.NewRecorder()
@ -671,7 +670,7 @@ func TestChallenge(t *testing.T) {
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
responseWriter := httptest.NewRecorder()
var key jose.JsonWebKey
@ -707,7 +706,7 @@ func TestNewRegistration(t *testing.T) {
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL
@ -879,7 +878,7 @@ func makeRevokeRequestJSON() ([]byte, error) {
// registration when GetRegistrationByKey is called, and we want to get a
// NoSuchRegistrationError for tests that pass regCheck = false to verifyPOST.
type mockSANoSuchRegistration struct {
mocks.MockSA
mocks.StorageAuthority
}
func (msa mockSANoSuchRegistration) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) {
@ -902,7 +901,7 @@ func TestRevokeCertificateCertKey(t *testing.T) {
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
wfe := setupWFE(t)
wfe.SA = &mockSANoSuchRegistration{mocks.MockSA{}}
wfe.SA = &mockSANoSuchRegistration{mocks.StorageAuthority{}}
responseWriter := httptest.NewRecorder()
nonce, err := wfe.nonceService.Nonce()
@ -992,7 +991,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mockSANoSuchRegistration{mocks.MockSA{}}
wfe.SA = &mockSANoSuchRegistration{mocks.StorageAuthority{}}
wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL
responseWriter := httptest.NewRecorder()
@ -1013,7 +1012,7 @@ func TestAuthorization(t *testing.T) {
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient()
responseWriter := httptest.NewRecorder()
@ -1101,7 +1100,7 @@ func TestRegistration(t *testing.T) {
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL
responseWriter := httptest.NewRecorder()
@ -1192,7 +1191,7 @@ func TestTermsRedirect(t *testing.T) {
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL
@ -1228,59 +1227,64 @@ func TestGetCertificate(t *testing.T) {
wfe := setupWFE(t)
wfe.CertCacheDuration = time.Second * 10
wfe.CertNoCacheExpirationWindow = time.Hour * 24 * 7
wfe.SA = &mocks.MockSA{}
wfe.SA = &mocks.StorageAuthority{}
certPemBytes, _ := ioutil.ReadFile("test/178.crt")
certBlock, _ := pem.Decode(certPemBytes)
responseWriter := httptest.NewRecorder()
mockLog := wfe.log.SyslogWriter.(*mocks.SyslogWriter)
mockLog.Clear()
// Valid serial, cached
path, _ := url.Parse("/acme/cert/0000000000000000000000000000000000b2")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
req, _ := http.NewRequest("GET", "/acme/cert/0000000000000000000000000000000000b2", nil)
req.RemoteAddr = "192.168.0.1"
wfe.Certificate(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 200)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/pkix-cert")
test.Assert(t, bytes.Compare(responseWriter.Body.Bytes(), certBlock.Bytes) == 0, "Certificates don't match")
reqlogs := mockLog.GetAllMatching(`Successful request`)
test.AssertEquals(t, len(reqlogs), 1)
test.AssertEquals(t, reqlogs[0].Priority, syslog.LOG_INFO)
test.AssertContains(t, reqlogs[0].Message, `"ClientAddr":"192.168.0.1"`)
// Unused serial, no cache
mockLog.Clear()
responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/0000000000000000000000000000000000ff")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
req, _ = http.NewRequest("GET", "/acme/cert/0000000000000000000000000000000000ff", nil)
req.RemoteAddr = "192.168.0.1"
req.Header.Set("X-Forwarded-For", "192.168.99.99")
wfe.Certificate(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
reqlogs = mockLog.GetAllMatching(`Terminated request`)
test.AssertEquals(t, len(reqlogs), 1)
test.AssertEquals(t, reqlogs[0].Priority, syslog.LOG_INFO)
test.AssertContains(t, reqlogs[0].Message, `"ClientAddr":"192.168.99.99,192.168.0.1"`)
// Invalid serial, no cache
responseWriter = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/acme/cert/nothex", nil)
wfe.Certificate(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
// Invalid serial, no cache
responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/nothex")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
// Invalid serial, no cache
responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/00000000000000")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
req, _ = http.NewRequest("GET", "/acme/cert/00000000000000", nil)
wfe.Certificate(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
}
func assertCsrLogged(t *testing.T, mockLog *mocks.MockSyslogWriter) {
func assertCsrLogged(t *testing.T, mockLog *mocks.SyslogWriter) {
matches := mockLog.GetAllMatching("^\\[AUDIT\\] Certificate request JSON=")
test.Assert(t, len(matches) == 1,
fmt.Sprintf("Incorrect number of certificate request log entries: %d",
@ -1297,20 +1301,25 @@ func TestLogCsrPem(t *testing.T) {
err := json.Unmarshal([]byte(certificateRequestJSON), &certificateRequest)
test.AssertNotError(t, err, "Unable to parse certificateRequest")
mockSA := mocks.MockSA{}
mockSA := mocks.StorageAuthority{}
reg, err := mockSA.GetRegistration(789)
test.AssertNotError(t, err, "Unable to get registration")
remoteAddr := "12.34.98.76"
req, err := http.NewRequest("GET", "http://[::1]/", nil)
test.AssertNotError(t, err, "NewRequest failed")
req.RemoteAddr = "12.34.98.76"
req.Header.Set("X-Forwarded-For", "10.0.0.1,172.16.0.1")
wfe.logCsr(remoteAddr, certificateRequest, reg)
mockLog := wfe.log.SyslogWriter.(*mocks.SyslogWriter)
mockLog.Clear()
wfe.logCsr(req, certificateRequest, reg)
mockLog := wfe.log.SyslogWriter.(*mocks.MockSyslogWriter)
matches := mockLog.GetAllMatching("Certificate request")
test.Assert(t, len(matches) == 1,
"Incorrect number of certificate request log entries")
test.AssertEquals(t, matches[0].Priority, syslog.LOG_NOTICE)
test.AssertEquals(t, matches[0].Message, `[AUDIT] Certificate request JSON={"RemoteAddr":"12.34.98.76","CsrBase64":"MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycX3ca+fViOuRWF38mssORISFxbJvspDfhPGRBZDxJ63NIqQzupB+6dp48xkcX7Z/KDaRJStcpJT2S0u33moNT4FHLklQBETLhExDk66cmlz6Xibp3LGZAwhWuec7wJoEwIgY8oq4rxihIyGq7HVIJoq9DqZGrUgfZMDeEJqbphukQOaXGEop7mD+eeu8+z5EVkB1LiJ6Yej6R8MAhVPHzG5fyOu6YVo6vY6QgwjRLfZHNj5XthxgPIEETZlUbiSoI6J19GYHvLURBTy5Ys54lYAPIGfNwcIBAH4gtH9FrYcDY68R22rp4iuxdvkf03ZWiT0F2W1y7/C9B2jayTzvQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAHd6Do9DIZ2hvdt1GwBXYjsqprZidT/DYOMfYcK17KlvdkFT58XrBH88ulLZ72NXEpiFMeTyzfs3XEyGq/Bbe7TBGVYZabUEh+LOskYwhgcOuThVN7tHnH5rhN+gb7cEdysjTb1QL+vOUwYgV75CB6PE5JVYK+cQsMIVvo0Kz4TpNgjJnWzbcH7h0mtvub+fCv92vBPjvYq8gUDLNrok6rbg05tdOJkXsF2G/W+Q6sf2Fvx0bK5JeH4an7P7cXF9VG9nd4sRt5zd+L3IcyvHVKxNhIJXZVH0AOqh/1YrKI9R0QKQiZCEy0xN1okPlcaIVaFhb7IKAHPxTI3r5f72LXY=","Registration":{"id":789,"key":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"agreement":"http://example.invalid/terms"}}`)
test.AssertEquals(t, matches[0].Message, `[AUDIT] Certificate request JSON={"ClientAddr":"10.0.0.1,172.16.0.1,12.34.98.76","CsrBase64":"MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycX3ca+fViOuRWF38mssORISFxbJvspDfhPGRBZDxJ63NIqQzupB+6dp48xkcX7Z/KDaRJStcpJT2S0u33moNT4FHLklQBETLhExDk66cmlz6Xibp3LGZAwhWuec7wJoEwIgY8oq4rxihIyGq7HVIJoq9DqZGrUgfZMDeEJqbphukQOaXGEop7mD+eeu8+z5EVkB1LiJ6Yej6R8MAhVPHzG5fyOu6YVo6vY6QgwjRLfZHNj5XthxgPIEETZlUbiSoI6J19GYHvLURBTy5Ys54lYAPIGfNwcIBAH4gtH9FrYcDY68R22rp4iuxdvkf03ZWiT0F2W1y7/C9B2jayTzvQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAHd6Do9DIZ2hvdt1GwBXYjsqprZidT/DYOMfYcK17KlvdkFT58XrBH88ulLZ72NXEpiFMeTyzfs3XEyGq/Bbe7TBGVYZabUEh+LOskYwhgcOuThVN7tHnH5rhN+gb7cEdysjTb1QL+vOUwYgV75CB6PE5JVYK+cQsMIVvo0Kz4TpNgjJnWzbcH7h0mtvub+fCv92vBPjvYq8gUDLNrok6rbg05tdOJkXsF2G/W+Q6sf2Fvx0bK5JeH4an7P7cXF9VG9nd4sRt5zd+L3IcyvHVKxNhIJXZVH0AOqh/1YrKI9R0QKQiZCEy0xN1okPlcaIVaFhb7IKAHPxTI3r5f72LXY=","Registration":{"id":789,"key":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"agreement":"http://example.invalid/terms"}}`)
}
func TestLengthRequired(t *testing.T) {
@ -1325,7 +1334,7 @@ func TestLengthRequired(t *testing.T) {
}
type mockSADifferentStoredKey struct {
mocks.MockSA
mocks.StorageAuthority
}
func (sa mockSADifferentStoredKey) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) {
@ -1340,7 +1349,7 @@ func (sa mockSADifferentStoredKey) GetRegistrationByKey(jwk jose.JsonWebKey) (co
func TestVerifyPOSTUsesStoredKey(t *testing.T) {
wfe := setupWFE(t)
wfe.SA = &mockSADifferentStoredKey{mocks.MockSA{}}
wfe.SA = &mockSADifferentStoredKey{mocks.StorageAuthority{}}
// signRequest signs with test1Key, but our special mock returns a
// registration with test2Key
_, _, _, err := wfe.verifyPOST(makePostRequest(signRequest(t, `{"resource":"foo"}`, &wfe.nonceService)), true, "foo")