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

View File

@ -130,6 +130,21 @@ func revokeByReg(regID int64, reasonCode core.RevocationCode, deny bool, rac rpc
return 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() { func main() {
app := cli.NewApp() app := cli.NewApp()
app.Name = "admin-revoker" app.Name = "admin-revoker"
@ -219,7 +234,7 @@ func main() {
Name: "list-reasons", Name: "list-reasons",
Usage: "List all revocation reason codes", Usage: "List all revocation reason codes",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
var codes core.RevocationCodes var codes revocationCodes
for k := range core.RevocationReasons { for k := range core.RevocationReasons {
codes = append(codes, k) codes = append(codes, k)
} }

View File

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

View File

@ -145,7 +145,7 @@ func TestCheckCert(t *testing.T) {
} }
delete(problemsMap, p) delete(problemsMap, p)
} }
for k, _ := range problemsMap { for k := range problemsMap {
t.Errorf("Found unexpected problem '%s'.", k) 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 { type fakeRegStore struct {
RegById map[int64]core.Registration RegByID map[int64]core.Registration
} }
func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) { func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) {
r, ok := f.RegById[id] r, ok := f.RegByID[id]
if !ok { if !ok {
msg := fmt.Sprintf("no such registration %d", id) msg := fmt.Sprintf("no such registration %d", id)
return r, core.NoSuchRegistrationError(msg) return r, core.NoSuchRegistrationError(msg)
@ -69,7 +69,7 @@ func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) {
} }
func newFakeRegStore() fakeRegStore { 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}})` const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`

View File

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "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. // 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() log := blog.GetAuditLogger()
// Check that this request is for the proper CA // Check that this request is for the proper CA
if bytes.Compare(req.IssuerKeyHash, src.caKeyHash) != 0 { if bytes.Compare(req.IssuerKeyHash, src.caKeyHash) != 0 {
log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash))) log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash)))
present = false return nil, false
return
} }
serialString := core.SerialToString(req.SerialNumber) 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;", err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;",
map[string]interface{}{"serial": serialString}) map[string]interface{}{"serial": serialString})
if err != nil { if err != nil {
present = false return nil, false
return
} }
log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString)) log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString))
response = ocspResponse.Response return ocspResponse.Response, true
present = true
return
} }
func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (cfocsp.Source, error) { func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (*DBSource, error) {
var noSource cfocsp.Source
// Configure DB // Configure DB
dbMap, err := sa.NewDbMap(dbConnect) dbMap, err := sa.NewDbMap(dbConnect)
if err != nil { 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) sa.SetSQLDebug(dbMap, sqlDebug)
// Load the CA's key so we can store its SubjectKey in the DB // Load the CA's key so we can store its SubjectKey in the DB
caCertDER, err := cmd.LoadCert(issuerCert) caCertDER, err := cmd.LoadCert(issuerCert)
if err != nil { 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) caCert, err := x509.ParseCertificate(caCertDER)
if err != nil { 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 { if len(caCert.SubjectKeyId) == 0 {
return noSource, fmt.Errorf("Empty subjectKeyID") return nil, fmt.Errorf("Empty subjectKeyID")
} }
// Construct source from DB // Construct source from DB
@ -161,6 +157,8 @@ func main() {
} }
source, err = cfocsp.NewSourceFromFile(filename) source, err = cfocsp.NewSourceFromFile(filename)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path)) 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) stopTimeout, err := time.ParseDuration(c.OCSPResponder.ShutdownStopTimeout)

View File

@ -1,16 +1,23 @@
package main package main
import ( import (
"bytes"
"fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"testing" "testing"
"time" "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) { func TestCacheControl(t *testing.T) {
src := make(ocsp.InMemorySource) src := make(cfocsp.InMemorySource)
h := handler(src, 10*time.Second) h := handler(src, 10*time.Second)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "/", nil) 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) 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" "log"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof" // HTTP performance profiling, added transparently to HTTP APIs
"os" "os"
"runtime" "runtime"
"time" "time"
@ -42,6 +42,7 @@ import (
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/publisher" "github.com/letsencrypt/boulder/publisher"
"github.com/letsencrypt/boulder/va"
) )
// Config stores configuration parameters that applications // Config stores configuration parameters that applications
@ -114,11 +115,7 @@ type Config struct {
VA struct { VA struct {
UserAgent string UserAgent string
PortConfig struct { PortConfig va.PortConfig
SimpleHTTPPort int
SimpleHTTPSPort int
DVSNIPort int
}
MaxConcurrentRPCServerRequests int64 MaxConcurrentRPCServerRequests int64
@ -220,6 +217,9 @@ type Config struct {
SubscriberAgreementURL string SubscriberAgreementURL string
} }
// CAConfig structs have configuration information for the certificate
// authority, including database parameters as well as controls for
// issued certificates.
type CAConfig struct { type CAConfig struct {
Profile string Profile string
TestMode bool TestMode bool
@ -242,6 +242,8 @@ type CAConfig struct {
DebugAddr string DebugAddr string
} }
// PAConfig specifies how a policy authority should connect to its
// database, and what policies it should enforce.
type PAConfig struct { type PAConfig struct {
DBConnect string DBConnect string
EnforcePolicyWhitelist bool EnforcePolicyWhitelist bool
@ -301,6 +303,7 @@ type AppShell struct {
App *cli.App App *cli.App
} }
// Version returns a string representing the version of boulder running.
func Version() string { func Version() string {
return fmt.Sprintf("0.1.0 [%s]", core.GetBuildID()) return fmt.Sprintf("0.1.0 [%s]", core.GetBuildID())
} }
@ -412,6 +415,11 @@ func LoadCert(path string) (cert []byte, err error) {
return 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) { func DebugServer(addr string) {
if addr == "" { if addr == "" {
log.Fatalf("unable to boot debug server because no address was given for it. Set debugAddr.") 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)) 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 { type ConfigDuration struct {
time.Duration 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") 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 { func (d *ConfigDuration) UnmarshalJSON(b []byte) error {
s := "" s := ""
err := json.Unmarshal(b, &s) err := json.Unmarshal(b, &s)
@ -444,10 +459,13 @@ func (d *ConfigDuration) UnmarshalJSON(b []byte) error {
return err return err
} }
// MarshalJSON returns the string form of the duration, as a byte array.
func (d ConfigDuration) MarshalJSON() ([]byte, error) { func (d ConfigDuration) MarshalJSON() ([]byte, error) {
return []byte(d.Duration.String()), nil 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 { func (d *ConfigDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string var s string
if err := unmarshal(&s); err != nil { if err := unmarshal(&s); err != nil {

View File

@ -5,31 +5,45 @@
package core package core
// SimpleHTTPChallenge constructs a random HTTP challenge import (
func SimpleHTTPChallenge() Challenge { "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
tls := true )
func newChallenge(challengeType string, accountKey *jose.JsonWebKey) Challenge {
return Challenge{ return Challenge{
Type: ChallengeTypeSimpleHTTP, Type: challengeType,
Status: StatusPending, Status: StatusPending,
Token: NewToken(), AccountKey: accountKey,
TLS: &tls, 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 // DvsniChallenge constructs a random DVSNI challenge
func DvsniChallenge() Challenge { // TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this method
return Challenge{ func DvsniChallenge(accountKey *jose.JsonWebKey) Challenge {
Type: ChallengeTypeDVSNI, return newChallenge(ChallengeTypeDVSNI, accountKey)
Status: StatusPending,
Token: NewToken(),
}
} }
// DNSChallenge constructs a random DNS challenge // HTTPChallenge01 constructs a random http-01 challenge
func DNSChallenge() Challenge { func HTTPChallenge01(accountKey *jose.JsonWebKey) Challenge {
return Challenge{ return newChallenge(ChallengeTypeHTTP01, accountKey)
Type: ChallengeTypeDNS, }
Status: StatusPending,
Token: NewToken(), // 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" "fmt"
"testing" "testing"
"time" "time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
) )
// challenges.go // 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) { func TestChallenges(t *testing.T) {
simpleHTTP := SimpleHTTPChallenge() var accountKey *jose.JsonWebKey
if simpleHTTP.Status != StatusPending { err := json.Unmarshal([]byte(accountKeyJSON), &accountKey)
t.Errorf("Incorrect status for challenge: %v", simpleHTTP.Status) if err != nil {
} t.Errorf("Error unmarshaling JWK: %v", err)
if len(simpleHTTP.Token) != 43 {
t.Errorf("Incorrect length for simpleHTTP token: %v", simpleHTTP.Token)
} }
dvsni := DvsniChallenge() simpleHTTP := SimpleHTTPChallenge(accountKey)
if dvsni.Status != StatusPending { if !simpleHTTP.IsSane(false) {
t.Errorf("Incorrect status for challenge: %v", dvsni.Status) 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 { if probe.Validated != merged.Validated {
t.Errorf("MergeChallenge allowed response to overwrite completed time") t.Errorf("MergeChallenge allowed response to overwrite completed time")
} }
if probe.Token != merged.Token { if probe.KeyAuthorization != merged.KeyAuthorization {
t.Errorf("MergeChallenge allowed response to overwrite status") t.Errorf("MergeChallenge allowed response to overwrite authorized key")
} }
if probe.TLS != merged.TLS { if probe.TLS != merged.TLS {
t.Errorf("MergeChallenge failed to overwrite 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 // PolicyAuthority defines the public interface for the Boulder PA
type PolicyAuthority interface { type PolicyAuthority interface {
WillingToIssue(id AcmeIdentifier, regID int64) error 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 // StorageGetter are the Boulder SA's read-only methods

View File

@ -6,6 +6,7 @@
package core package core
import ( import (
"crypto/subtle"
"crypto/x509" "crypto/x509"
"encoding/asn1" "encoding/asn1"
"encoding/base64" "encoding/base64"
@ -91,11 +92,13 @@ const (
const ( const (
ChallengeTypeSimpleHTTP = "simpleHttp" ChallengeTypeSimpleHTTP = "simpleHttp"
ChallengeTypeDVSNI = "dvsni" ChallengeTypeDVSNI = "dvsni"
ChallengeTypeDNS = "dns" ChallengeTypeHTTP01 = "http-01"
ChallengeTypeTLSSNI01 = "tls-sni-01"
ChallengeTypeDNS01 = "dns-01"
) )
// The suffix appended to pseudo-domain names in DVSNI challenges // 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 // The label attached to DNS names in DNS challenges
const DNSPrefix = "_acme-challenge" const DNSPrefix = "_acme-challenge"
@ -192,6 +195,97 @@ type ValidationRecord struct {
AddressUsed net.IP `json:"addressUsed"` 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. // Challenge is an aggregate of all data needed for any challenges.
// //
// Rather than define individual types for different types of // 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 // A URI to which a response can be POSTed
URI string `json:"uri"` 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"` Token string `json:"token,omitempty"`
// Used by simpleHTTP challenges // Used by simpleHttp challenges
TLS *bool `json:"tls,omitempty"` TLS *bool `json:"tls,omitempty"`
// Used by dns and dvsni challenges // Used by dvsni challenges
Validation *jose.JsonWebSignature `json:"validation,omitempty"` 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 // Contains information about URLs used or redirected to and IPs resolved and
// used // used
ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"` ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"`
@ -249,6 +346,9 @@ func (ch Challenge) RecordsSane() bool {
switch ch.Type { switch ch.Type {
case ChallengeTypeSimpleHTTP: case ChallengeTypeSimpleHTTP:
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this case
fallthrough
case ChallengeTypeHTTP01:
for _, rec := range ch.ValidationRecord { for _, rec := range ch.ValidationRecord {
if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil || if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil ||
len(rec.AddressesResolved) == 0 { len(rec.AddressesResolved) == 0 {
@ -256,6 +356,9 @@ func (ch Challenge) RecordsSane() bool {
} }
} }
case ChallengeTypeDVSNI: case ChallengeTypeDVSNI:
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this case
fallthrough
case ChallengeTypeTLSSNI01:
if len(ch.ValidationRecord) > 1 { if len(ch.ValidationRecord) > 1 {
return false return false
} }
@ -266,16 +369,25 @@ func (ch Challenge) RecordsSane() bool {
ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 { ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
return false return false
} }
case ChallengeTypeDNS: case ChallengeTypeDNS01:
// Nothing for now // Nothing for now
} }
return true return true
} }
// IsSane checks the sanity of a challenge object before issued to the client // isLegacy returns true if the challenge is of a legacy type (i.e., one defined
// (completed = false) and before validation (completed = true). // before draft-ietf-acme-acme-00)
func (ch Challenge) IsSane(completed bool) bool { // 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 { if ch.Status != StatusPending {
return false return false
} }
@ -303,9 +415,6 @@ func (ch Challenge) IsSane(completed bool) bool {
return false return false
} }
case ChallengeTypeDVSNI: case ChallengeTypeDVSNI:
// Same as DNS
fallthrough
case ChallengeTypeDNS:
// check extra fields aren't used // check extra fields aren't used
if ch.TLS != nil { if ch.TLS != nil {
return false return false
@ -330,9 +439,10 @@ func (ch Challenge) IsSane(completed bool) bool {
return true 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 '.' // 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 { switch ch.Type {
case ChallengeTypeSimpleHTTP: case ChallengeTypeSimpleHTTP:
// For simpleHttp, only "tls" is client-provided // For simpleHttp, only "tls" is client-provided
@ -345,8 +455,6 @@ func (ch Challenge) MergeResponse(resp Challenge) Challenge {
} }
case ChallengeTypeDVSNI: case ChallengeTypeDVSNI:
fallthrough
case ChallengeTypeDNS:
// For dvsni and dns, only "validation" is client-provided // For dvsni and dns, only "validation" is client-provided
if resp.Validation != nil { if resp.Validation != nil {
ch.Validation = resp.Validation ch.Validation = resp.Validation
@ -356,6 +464,65 @@ func (ch Challenge) MergeResponse(resp Challenge) Challenge {
return ch 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 // Authorization represents the authorization of an account key holder
// to act on behalf of a domain. This struct is intended to be used both // 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 // 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"` Expires time.Time `db:"expires"`
} }
type IssuedCertIdentifierData struct {
ReversedName string
Serial string
}
// IdentifierData holds information about what certificates are known for a // IdentifierData holds information about what certificates are known for a
// given identifier. This is used to present Proof of Posession challenges in // given identifier. This is used to present Proof of Posession challenges in
// the case where a certificate already exists. The DB table holding // the case where a certificate already exists. The DB table holding
@ -557,6 +719,8 @@ type OCSPSigningRequest struct {
RevokedAt time.Time 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 { type SignedCertificateTimestamp struct {
ID int `db:"id"` ID int `db:"id"`
// The version of the protocol to which the SCT conforms // The version of the protocol to which the SCT conforms
@ -577,8 +741,6 @@ type SignedCertificateTimestamp struct {
LockCol int64 LockCol int64
} }
type RPCSignedCertificateTimestamp SignedCertificateTimestamp
type rawSignedCertificateTimestamp struct { type rawSignedCertificateTimestamp struct {
Version uint8 `json:"sct_version"` Version uint8 `json:"sct_version"`
LogID string `json:"id"` LogID string `json:"id"`
@ -587,6 +749,9 @@ type rawSignedCertificateTimestamp struct {
Extensions string `json:"extensions"` 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 { func (sct *SignedCertificateTimestamp) UnmarshalJSON(data []byte) error {
var err error var err error
var rawSCT rawSignedCertificateTimestamp var rawSCT rawSignedCertificateTimestamp
@ -649,20 +814,6 @@ func (sct *SignedCertificateTimestamp) CheckSignature() error {
// RevocationCode is used to specify a certificate revocation reason // RevocationCode is used to specify a certificate revocation reason
type RevocationCode int 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 // RevocationReasons provides a map from reason code to string explaining the
// code // code
var RevocationReasons = map[RevocationCode]string{ var RevocationReasons = map[RevocationCode]string{

View File

@ -6,6 +6,8 @@
package core package core
import ( import (
"crypto/rand"
"crypto/rsa"
"encoding/json" "encoding/json"
"net" "net"
"testing" "testing"
@ -40,6 +42,24 @@ func TestRegistrationUpdate(t *testing.T) {
test.Assert(t, reg.Agreement == update.Agreement, "Agreement was not updated") 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) { func TestRecordSanityCheck(t *testing.T) {
rec := []ValidationRecord{ rec := []ValidationRecord{
ValidationRecord{ ValidationRecord{
@ -66,7 +86,8 @@ func TestRecordSanityCheck(t *testing.T) {
test.Assert(t, !chall.RecordsSane(), "Record should not be sane") 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 // Make a temporary account key
var accountKey *jose.JsonWebKey var accountKey *jose.JsonWebKey
err := json.Unmarshal([]byte(`{ err := json.Unmarshal([]byte(`{
@ -76,7 +97,7 @@ func TestChallengeSanityCheck(t *testing.T) {
}`), &accountKey) }`), &accountKey)
test.AssertNotError(t, err, "Error unmarshaling JWK") test.AssertNotError(t, err, "Error unmarshaling JWK")
types := []string{ChallengeTypeSimpleHTTP, ChallengeTypeDVSNI, ChallengeTypeDNS} types := []string{ChallengeTypeSimpleHTTP, ChallengeTypeDVSNI}
for _, challengeType := range types { for _, challengeType := range types {
chall := Challenge{ chall := Challenge{
Type: challengeType, Type: challengeType,
@ -107,7 +128,7 @@ func TestChallengeSanityCheck(t *testing.T) {
AddressUsed: net.IP{127, 0, 0, 1}, AddressUsed: net.IP{127, 0, 0, 1},
}} }}
test.Assert(t, chall.IsSane(true), "IsSane should be true") 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) chall.Validation = new(jose.JsonWebSignature)
if challengeType == ChallengeTypeDVSNI { if challengeType == ChallengeTypeDVSNI {
chall.ValidationRecord = []ValidationRecord{ValidationRecord{ chall.ValidationRecord = []ValidationRecord{ValidationRecord{
@ -128,6 +149,43 @@ func TestChallengeSanityCheck(t *testing.T) {
test.Assert(t, !chall.IsSane(true), "IsSane should be false") 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) { func TestJSONBufferUnmarshal(t *testing.T) {
testStruct := struct { testStruct := struct {
Buffer JSONBuffer Buffer JSONBuffer

View File

@ -8,6 +8,7 @@ package core
import ( import (
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha1" "crypto/sha1"
@ -26,6 +27,7 @@ import (
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"net/url" "net/url"
"regexp"
"strings" "strings"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
@ -153,6 +155,14 @@ func NewToken() string {
return RandomString(32) 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 // Fingerprints
// Fingerprint256 produces an unpadded, URL-safe Base64-encoded SHA256 digest // Fingerprint256 produces an unpadded, URL-safe Base64-encoded SHA256 digest
@ -163,6 +173,96 @@ func Fingerprint256(data []byte) string {
return B64enc(d.Sum(nil)) 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 // KeyDigest produces a padded, standard Base64-encoded SHA256 digest of a
// provided public key. // provided public key.
func KeyDigest(key crypto.PublicKey) (string, error) { 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 // AcmeURL is a URL that automatically marshal/unmarshal to JSON strings
type AcmeURL url.URL type AcmeURL url.URL
// ParseAcmeURL is just a wrapper around url.Parse that returns an *AcmeURL
func ParseAcmeURL(s string) (*AcmeURL, error) { func ParseAcmeURL(s string) (*AcmeURL, error) {
u, err := url.Parse(s) u, err := url.Parse(s)
if err != nil { if err != nil {
@ -327,6 +428,9 @@ func StringToSerial(serial string) (*big.Int, error) {
return &serialNum, err 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 { func ValidSerial(serial string) bool {
// Originally, serial numbers were 32 hex characters long. We later increased // 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 // 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 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) { func TestSerialUtils(t *testing.T) {
serial := SerialToString(big.NewInt(100000000000000000)) serial := SerialToString(big.NewInt(100000000000000000))
test.AssertEquals(t, serial, "00000000000000000000016345785d8a0000") test.AssertEquals(t, serial, "00000000000000000000016345785d8a0000")
@ -58,12 +64,22 @@ const JWK1JSON = `{
"e": "AQAB" "e": "AQAB"
}` }`
const JWK1Digest = `ul04Iq07ulKnnrebv2hv3yxCGgVvoHs8hjq2tVKx3mc=` const JWK1Digest = `ul04Iq07ulKnnrebv2hv3yxCGgVvoHs8hjq2tVKx3mc=`
const JWK1Thumbprint = `-kVpHjJCDNQQk-j9BGMpzHAVCiOqvoTRZB-Ov4CAiM4`
const JWK2JSON = `{ const JWK2JSON = `{
"kty":"RSA", "kty":"RSA",
"n":"yTsLkI8n4lg9UuSKNRC0UPHsVjNdCYk8rGXIqeb_rRYaEev3D9-kxXY8HrYfGkVt5CiIVJ-n2t50BKT8oBEMuilmypSQqJw0pCgtUm-e6Z0Eg3Ly6DMXFlycyikegiZ0b-rVX7i5OCEZRDkENAYwFNX4G7NNCwEZcH7HUMUmty9dchAqDS9YWzPh_dde1A9oy9JMH07nRGDcOzIh1rCPwc71nwfPPYeeS4tTvkjanjeigOYBFkBLQuv7iBB4LPozsGF1XdoKiIIi-8ye44McdhOTPDcQp3xKxj89aO02pQhBECv61rmbPinvjMG9DYxJmZvjsKF4bN2oy0DxdC1jDw", "n":"yTsLkI8n4lg9UuSKNRC0UPHsVjNdCYk8rGXIqeb_rRYaEev3D9-kxXY8HrYfGkVt5CiIVJ-n2t50BKT8oBEMuilmypSQqJw0pCgtUm-e6Z0Eg3Ly6DMXFlycyikegiZ0b-rVX7i5OCEZRDkENAYwFNX4G7NNCwEZcH7HUMUmty9dchAqDS9YWzPh_dde1A9oy9JMH07nRGDcOzIh1rCPwc71nwfPPYeeS4tTvkjanjeigOYBFkBLQuv7iBB4LPozsGF1XdoKiIIi-8ye44McdhOTPDcQp3xKxj89aO02pQhBECv61rmbPinvjMG9DYxJmZvjsKF4bN2oy0DxdC1jDw",
"e":"AQAB" "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) { func TestKeyDigest(t *testing.T) {
// Test with JWK (value, reference, and direct) // Test with JWK (value, reference, and direct)
var jwk jose.JsonWebKey var jwk jose.JsonWebKey

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import (
"regexp" "regexp"
"strings" "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/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
@ -198,14 +199,14 @@ func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64
// acceptable for the given identifier. // acceptable for the given identifier.
// //
// Note: Current implementation is static, but future versions may not be. // 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{ challenges = []core.Challenge{
core.SimpleHTTPChallenge(), core.SimpleHTTPChallenge(accountKey),
core.DvsniChallenge(), core.DvsniChallenge(accountKey),
} core.HTTPChallenge01(accountKey),
combinations = [][]int{ core.TLSSNIChallenge01(accountKey),
[]int{0},
[]int{1},
} }
combinations = [][]int{[]int{0}, []int{1}, []int{2}, []int{3}}
return return
} }

View File

@ -6,8 +6,11 @@
package policy package policy
import ( import (
"encoding/json"
"testing" "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/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks" "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) { func TestChallengesFor(t *testing.T) {
pa, cleanup := paImpl(t) pa, cleanup := paImpl(t)
defer cleanup() 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, combinations, err := pa.ChallengesFor(core.AcmeIdentifier{}, accountKey)
challenges[1].Type != core.ChallengeTypeDVSNI { 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") 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") t.Error("Incorrect combinations returned")
} }
} }

View File

@ -24,6 +24,7 @@ import (
"github.com/letsencrypt/boulder/sa" "github.com/letsencrypt/boulder/sa"
) )
// LogDescription tells you how to connect to a log and verify its statements.
type LogDescription struct { type LogDescription struct {
ID string ID string
URI string URI string
@ -35,6 +36,9 @@ type rawLogDescription struct {
PublicKey string `json:"key"` 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 { func (logDesc *LogDescription) UnmarshalJSON(data []byte) error {
var rawLogDesc rawLogDescription var rawLogDesc rawLogDescription
if err := json.Unmarshal(data, &rawLogDesc); err != nil { 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") test.AssertNotError(t, err, "Couldn't create new Publisher")
pub.issuerBundle = append(pub.issuerBundle, base64.StdEncoding.EncodeToString(intermediatePEM.Bytes)) pub.issuerBundle = append(pub.issuerBundle, base64.StdEncoding.EncodeToString(intermediatePEM.Bytes))
pub.SA = &mocks.MockSA{} pub.SA = &mocks.StorageAuthority{}
leafPEM, _ := pem.Decode([]byte(testLeaf)) leafPEM, _ := pem.Decode([]byte(testLeaf))
leaf, err := x509.ParseCertificate(leafPEM.Bytes) leaf, err := x509.ParseCertificate(leafPEM.Bytes)

View File

@ -214,13 +214,8 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
return authz, err return authz, err
} }
// Create validations, but we have to update them with URIs later // Create validations. The WFE will update them with URIs before sending them out.
challenges, combinations := ra.PA.ChallengesFor(identifier) challenges, combinations, err := ra.PA.ChallengesFor(identifier, &reg.Key)
for i := range challenges {
// Add the account key used to generate the challenge
challenges[i].AccountKey = &reg.Key
}
expires := ra.clk.Now().Add(ra.authorizationLifetime) 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) 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 // Store the updated version
if err = ra.SA.UpdatePendingAuthorization(authz); err != nil { if err = ra.SA.UpdatePendingAuthorization(authz); err != nil {
// This can pretty much only happen when the client corrupts the Challenge // This can pretty much only happen when the client corrupts the Challenge

View File

@ -101,9 +101,6 @@ var (
} }
ResponseIndex = 0 ResponseIndex = 0
Response = core.Challenge{
Type: "simpleHttp",
}
ExampleCSR = &x509.CertificateRequest{} ExampleCSR = &x509.CertificateRequest{}
@ -116,11 +113,7 @@ var (
Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-example.com"}, Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-example.com"},
RegistrationID: 1, RegistrationID: 1,
Status: "pending", Status: "pending",
Challenges: []core.Challenge{ Combinations: [][]int{[]int{0}, []int{1}},
core.SimpleHTTPChallenge(),
core.DvsniChallenge(),
},
Combinations: [][]int{[]int{0}, []int{1}},
} }
AuthzFinal = core.Authorization{} AuthzFinal = core.Authorization{}
@ -132,6 +125,16 @@ const (
saDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test" 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()) { func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAuthority, *RegistrationAuthorityImpl, clock.FakeClock, func()) {
err := json.Unmarshal(AccountKeyJSONA, &AccountKeyA) err := json.Unmarshal(AccountKeyJSONA, &AccountKeyA)
test.AssertNotError(t, err, "Failed to unmarshal public JWK") 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") test.AssertNotError(t, err, "Failed to unmarshal private JWK")
err = json.Unmarshal(ShortKeyJSON, &ShortKey) 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() fc := clock.NewFake()
@ -195,7 +202,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
ValidityPeriod: time.Hour * 2190, ValidityPeriod: time.Hour * 2190,
NotAfter: time.Now().Add(time.Hour * 8761), NotAfter: time.Now().Add(time.Hour * 8761),
Clk: fc, Clk: fc,
Publisher: &mocks.MockPublisher{}, Publisher: &mocks.Publisher{},
} }
cleanUp := func() { cleanUp := func() {
saDBCleanUp() saDBCleanUp()
@ -218,7 +225,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
ra.VA = va ra.VA = va
ra.CA = &ca ra.CA = &ca
ra.PA = pa ra.PA = pa
ra.DNSResolver = &mocks.MockDNS{} ra.DNSResolver = &mocks.DNSResolver{}
AuthzInitial.RegistrationID = Registration.ID AuthzInitial.RegistrationID = Registration.ID
@ -256,38 +263,38 @@ func TestValidateContacts(t *testing.T) {
nStats, _ := statsd.NewNoopClient() nStats, _ := statsd.NewNoopClient()
err := validateContacts([]*core.AcmeURL{}, &mocks.MockDNS{}, nStats) err := validateContacts([]*core.AcmeURL{}, &mocks.DNSResolver{}, nStats)
test.AssertNotError(t, err, "No Contacts") 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") 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") 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") 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") 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") test.AssertError(t, err, "Unknown scehme")
} }
func TestValidateEmail(t *testing.T) { func TestValidateEmail(t *testing.T) {
_, err := validateEmail("an email`", &mocks.MockDNS{}) _, err := validateEmail("an email`", &mocks.DNSResolver{})
test.AssertError(t, err, "Malformed") 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") test.AssertError(t, err, "Cannot resolve")
t.Logf("No Resolve: %s", err) 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") test.AssertError(t, err, "No MX Record")
t.Logf("No MX: %s", err) 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") 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") test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
// TODO Verify that challenges are correct // 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[0].Type == core.ChallengeTypeSimpleHTTP, "Challenge 0 not SimpleHTTP")
test.Assert(t, authz.Challenges[1].Type == core.ChallengeTypeDVSNI, "Challenge 1 not DVSNI") 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[0].IsSane(false), "Challenge 0 is not sane")
test.Assert(t, authz.Challenges[1].IsSane(false), "Challenge 1 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") t.Log("DONE TestNewAuthorization")
} }
@ -394,7 +411,9 @@ func TestUpdateAuthorization(t *testing.T) {
authz, err := ra.NewAuthorization(AuthzRequest, Registration.ID) authz, err := ra.NewAuthorization(AuthzRequest, Registration.ID)
test.AssertNotError(t, err, "NewAuthorization failed") 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") test.AssertNotError(t, err, "UpdateAuthorization failed")
// Verify that returned authz same as DB // Verify that returned authz same as DB
@ -428,7 +447,9 @@ func TestUpdateAuthorizationReject(t *testing.T) {
test.AssertNotError(t, err, "UpdateRegistration failed") test.AssertNotError(t, err, "UpdateRegistration failed")
// Verify that the RA rejected the authorization request // 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")) test.AssertEquals(t, err, core.UnauthorizedError("Challenge cannot be updated with a different key"))
t.Log("DONE TestUpdateAuthorizationReject") t.Log("DONE TestUpdateAuthorizationReject")
@ -437,7 +458,9 @@ func TestUpdateAuthorizationReject(t *testing.T) {
func TestOnValidationUpdateSuccess(t *testing.T) { func TestOnValidationUpdateSuccess(t *testing.T) {
_, sa, ra, fclk, cleanUp := initAuthorities(t) _, sa, ra, fclk, cleanUp := initAuthorities(t)
defer cleanUp() 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) expires := fclk.Now().Add(300 * 24 * time.Hour)
authzUpdated.Expires = &expires authzUpdated.Expires = &expires
sa.UpdatePendingAuthorization(authzUpdated) sa.UpdatePendingAuthorization(authzUpdated)
@ -632,7 +655,7 @@ func TestDomainsForRateLimiting(t *testing.T) {
} }
type mockSAWithNameCounts struct { type mockSAWithNameCounts struct {
mocks.MockSA mocks.StorageAuthority
nameCounts map[string]int nameCounts map[string]int
t *testing.T t *testing.T
clk clock.FakeClock clk clock.FakeClock

View File

@ -200,16 +200,16 @@ func (rpc *AmqpRPCServer) Handle(method string, handler func([]byte) ([]byte, er
rpc.dispatchTable[method] = handler 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{}. // due to type interface{}.
type RPCError struct { type rpcError struct {
Value string `json:"value"` Value string `json:"value"`
Type string `json:"type,omitempty"` 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. // JSON.
func wrapError(err error) (rpcError RPCError) { func wrapError(err error) (rpcError rpcError) {
if err != nil { if err != nil {
rpcError.Value = err.Error() rpcError.Value = err.Error()
switch err.(type) { switch err.(type) {
@ -240,8 +240,8 @@ func wrapError(err error) (rpcError RPCError) {
return return
} }
// Unwraps a RPCError and returns the correct error type. // Unwraps a rpcError and returns the correct error type.
func unwrapError(rpcError RPCError) (err error) { func unwrapError(rpcError rpcError) (err error) {
if rpcError.Value != "" { if rpcError.Value != "" {
switch rpcError.Type { switch rpcError.Type {
case "InternalServerError": case "InternalServerError":
@ -273,11 +273,11 @@ func unwrapError(rpcError RPCError) (err error) {
return return
} }
// RPCResponse is a stuct for wire-representation of response messages // rpcResponse is a stuct for wire-representation of response messages
// used by DispatchSync // used by DispatchSync
type RPCResponse struct { type rpcResponse struct {
ReturnVal []byte `json:"returnVal,omitempty"` 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 // 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)) 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 return
} }
var response RPCResponse var response rpcResponse
var err error var err error
response.ReturnVal, err = cb(msg.Body) response.ReturnVal, err = cb(msg.Body)
response.Error = wrapError(err) 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 // until a fatal error is returned or AmqpRPCServer.Stop() is called and all
// remaining messages are processed. // remaining messages are processed.
func (rpc *AmqpRPCServer) Start(c cmd.Config) error { func (rpc *AmqpRPCServer) Start(c cmd.Config) error {
tooManyGoroutines := RPCResponse{ tooManyGoroutines := rpcResponse{
Error: wrapError(core.TooManyRPCRequestsError("RPC server has spawned too many Goroutines")), Error: wrapError(core.TooManyRPCRequestsError("RPC server has spawned too many Goroutines")),
} }
tooManyRequestsResponse, err := json.Marshal(tooManyGoroutines) tooManyRequestsResponse, err := json.Marshal(tooManyGoroutines)
@ -632,7 +632,7 @@ func (rpc *AmqpRPCCLient) DispatchSync(method string, body []byte) (response []b
callStarted := time.Now() callStarted := time.Now()
select { select {
case jsonResponse := <-rpc.Dispatch(method, body): case jsonResponse := <-rpc.Dispatch(method, body):
var rpcResponse RPCResponse var rpcResponse rpcResponse
err = json.Unmarshal(jsonResponse, &rpcResponse) err = json.Unmarshal(jsonResponse, &rpcResponse)
if err != nil { if err != nil {
return return

View File

@ -9,14 +9,14 @@ import (
"time" "time"
) )
// RPCClient describes the functions an RPC Client performs // Client describes the functions an RPC Client performs
type RPCClient interface { type Client interface {
SetTimeout(time.Duration) SetTimeout(time.Duration)
Dispatch(string, []byte) chan []byte Dispatch(string, []byte) chan []byte
DispatchSync(string, []byte) ([]byte, error) DispatchSync(string, []byte) ([]byte, error)
} }
// RPCServer describes the functions an RPC Server performs // Server describes the functions an RPC Server performs
type RPCServer interface { type Server interface {
Handle(string, func([]byte) ([]byte, error)) 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 // NewRegistrationAuthorityServer constructs an RPC server
func NewRegistrationAuthorityServer(rpc RPCServer, impl core.RegistrationAuthority) error { func NewRegistrationAuthorityServer(rpc Server, impl core.RegistrationAuthority) error {
log := blog.GetAuditLogger() log := blog.GetAuditLogger()
rpc.Handle(MethodNewRegistration, func(req []byte) (response []byte, err error) { 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 // RegistrationAuthorityClient represents an RA RPC client
type RegistrationAuthorityClient struct { type RegistrationAuthorityClient struct {
rpc RPCClient rpc Client
} }
// NewRegistrationAuthorityClient constructs an 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} rac = RegistrationAuthorityClient{rpc: client}
return return
} }
@ -496,7 +496,7 @@ func (rac RegistrationAuthorityClient) OnValidationUpdate(authz core.Authorizati
// //
// ValidationAuthorityClient / Server // ValidationAuthorityClient / Server
// -> UpdateValidations // -> 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) { rpc.Handle(MethodUpdateValidations, func(req []byte) (response []byte, err error) {
var vaReq validationRequest var vaReq validationRequest
if err = json.Unmarshal(req, &vaReq); err != nil { 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 // ValidationAuthorityClient represents an RPC client for the VA
type ValidationAuthorityClient struct { type ValidationAuthorityClient struct {
rpc RPCClient rpc Client
} }
// NewValidationAuthorityClient constructs an 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} vac = ValidationAuthorityClient{rpc: client}
return return
} }
@ -589,7 +589,8 @@ func (vac ValidationAuthorityClient) CheckCAARecords(ident core.AcmeIdentifier)
return 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) { rpc.Handle(MethodSubmitToCT, func(req []byte) (response []byte, err error) {
err = impl.SubmitToCT(req) err = impl.SubmitToCT(req)
return return
@ -600,11 +601,11 @@ func NewPublisherServer(rpc RPCServer, impl core.Publisher) (err error) {
// PublisherClient is a client to communicate with the Publisher Authority // PublisherClient is a client to communicate with the Publisher Authority
type PublisherClient struct { type PublisherClient struct {
rpc RPCClient rpc Client
} }
// NewPublisherClient constructs an 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} pub = PublisherClient{rpc: client}
return return
} }
@ -619,7 +620,7 @@ func (pub PublisherClient) SubmitToCT(der []byte) (err error) {
// //
// CertificateAuthorityClient / Server // CertificateAuthorityClient / Server
// -> IssueCertificate // -> 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) { rpc.Handle(MethodIssueCertificate, func(req []byte) (response []byte, err error) {
var icReq issueCertificateRequest var icReq issueCertificateRequest
err = json.Unmarshal(req, &icReq) 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. // CertificateAuthorityClient is a client to communicate with the CA.
type CertificateAuthorityClient struct { type CertificateAuthorityClient struct {
rpc RPCClient rpc Client
} }
// NewCertificateAuthorityClient constructs an 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} cac = CertificateAuthorityClient{rpc: client}
return return
} }
@ -752,7 +753,7 @@ func (cac CertificateAuthorityClient) GenerateOCSP(signRequest core.OCSPSigningR
} }
// NewStorageAuthorityServer constructs an RPC server // 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) { rpc.Handle(MethodUpdateRegistration, func(req []byte) (response []byte, err error) {
var reg core.Registration var reg core.Registration
if err = json.Unmarshal(req, &reg); err != nil { 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) 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 { if err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
errorCondition(MethodGetSCTReceipt, err, req) 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) { rpc.Handle(MethodAddSCTReceipt, func(req []byte) (response []byte, err error) {
var sct core.RPCSignedCertificateTimestamp var sct core.SignedCertificateTimestamp
err = json.Unmarshal(req, &sct) err = json.Unmarshal(req, &sct)
if err != nil { if err != nil {
// AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64 // 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 // StorageAuthorityClient is a client to communicate with the Storage Authority
type StorageAuthorityClient struct { type StorageAuthorityClient struct {
rpc RPCClient rpc Client
} }
// NewStorageAuthorityClient constructs an 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} sac = StorageAuthorityClient{rpc: client}
return 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 // 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 var lvar latestValidAuthorizationRequest
lvar.RegID = registrationId lvar.RegID = registrationID
lvar.Identifier = identifier lvar.Identifier = identifier
data, err := json.Marshal(lvar) data, err := json.Marshal(lvar)
@ -1369,6 +1370,8 @@ func (cac StorageAuthorityClient) CountCertificatesByNames(names []string, earli
return 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) { func (cac StorageAuthorityClient) GetSCTReceipt(serial string, logID string) (receipt core.SignedCertificateTimestamp, err error) {
var gsctReq struct { var gsctReq struct {
Serial string Serial string
@ -1391,6 +1394,7 @@ func (cac StorageAuthorityClient) GetSCTReceipt(serial string, logID string) (re
return return
} }
// AddSCTReceipt adds a new SCT to the database.
func (cac StorageAuthorityClient) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) { func (cac StorageAuthorityClient) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) {
data, err := json.Marshal(sct) data, err := json.Marshal(sct)
if err != nil { 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" "net/url"
"strings" "strings"
// Provide access to the MySQL driver
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/go-sql-driver/mysql" _ "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" gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core" "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 // 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 { type challModel struct {
ID int64 `db:"id"` ID int64 `db:"id"`
AuthorizationID string `db:"authorizationID"` AuthorizationID string `db:"authorizationID"`
@ -45,7 +47,7 @@ type challModel struct {
Validated *time.Time `db:"validated"` Validated *time.Time `db:"validated"`
Token string `db:"token"` Token string `db:"token"`
TLS *bool `db:"tls"` TLS *bool `db:"tls"`
Validation []byte `db:"validation"` KeyAuthorization string `db:"keyAuthorization"`
ValidationRecord []byte `db:"validationRecord"` ValidationRecord []byte `db:"validationRecord"`
AccountKey []byte `db:"accountKey"` AccountKey []byte `db:"accountKey"`
@ -99,11 +101,12 @@ func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
Token: c.Token, Token: c.Token,
TLS: c.TLS, TLS: c.TLS,
} }
if c.Validation != nil { if c.KeyAuthorization != nil {
cm.Validation = []byte(c.Validation.FullSerialize()) kaString := c.KeyAuthorization.String()
if len(cm.Validation) > mediumBlobSize { if len(kaString) > 255 {
return nil, fmt.Errorf("Validation object is too large to store in the database") return nil, fmt.Errorf("Key authorization is too large to store in the database")
} }
cm.KeyAuthorization = kaString
} }
if c.Error != nil { if c.Error != nil {
errJSON, err := json.Marshal(c.Error) errJSON, err := json.Marshal(c.Error)
@ -147,12 +150,12 @@ func modelToChallenge(cm *challModel) (core.Challenge, error) {
Token: cm.Token, Token: cm.Token,
TLS: cm.TLS, TLS: cm.TLS,
} }
if len(cm.Validation) > 0 { if len(cm.KeyAuthorization) > 0 {
val, err := jose.ParseSigned(string(cm.Validation)) ka, err := core.NewKeyAuthorizationFromString(cm.KeyAuthorization)
if err != nil { if err != nil {
return core.Challenge{}, err return core.Challenge{}, err
} }
c.Validation = val c.KeyAuthorization = &ka
} }
if len(cm.Error) > 0 { if len(cm.Error) > 0 {
var problem core.ProblemDetails 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 // 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) ident, err := json.Marshal(identifier)
if err != nil { if err != nil {
return return
} }
var auth core.Authorization var auth core.Authorization
err = ssa.dbMap.SelectOne(&auth, "SELECT id FROM authz "+ 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", "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 { if err != nil {
return return
} }
@ -229,6 +229,8 @@ func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationId int64
return ssa.GetAuthorization(auth.ID) 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 type TooManyCertificatesError string
func (t TooManyCertificatesError) Error() 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) { 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 // create pending auth
authz, err := sa.NewPendingAuthorization(core.Authorization{RegistrationID: regID, Challenges: []core.Challenge{core.Challenge{}}}) 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) reg := satest.CreateWorkingRegistration(t, sa)
// authorize "example.org" // authorize "example.org"
authz = CreateDomainAuthWithRegId(t, "example.org", sa, reg.ID) authz = CreateDomainAuthWithRegID(t, "example.org", sa, reg.ID)
// finalize auth // finalize auth
authz.Status = core.StatusValid authz.Status = core.StatusValid
@ -243,7 +243,7 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
reg := satest.CreateWorkingRegistration(t, sa) reg := satest.CreateWorkingRegistration(t, sa)
// create invalid authz // 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 exp := time.Now().AddDate(0, 0, 10) // expire in 10 day
authz.Expires = &exp authz.Expires = &exp
authz.Status = core.StatusInvalid 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) test.AssertError(t, err, "Should not have found a valid auth for "+domain)
// create valid auth // 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 exp = time.Now().AddDate(0, 0, 1) // expire in 1 day
authz.Expires = &exp authz.Expires = &exp
authz.Status = core.StatusValid authz.Status = core.StatusValid
@ -271,7 +271,7 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
test.AssertEquals(t, authz.RegistrationID, reg.ID) test.AssertEquals(t, authz.RegistrationID, reg.ID)
// create a newer auth // 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 exp = time.Now().AddDate(0, 0, 2) // expire in 2 day
newAuthz.Expires = &exp newAuthz.Expires = &exp
newAuthz.Status = core.StatusValid newAuthz.Status = core.StatusValid

24
test.sh
View File

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

View File

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

View File

@ -137,6 +137,12 @@ module.exports = {
return forge.pki.certificateRequestToPem(c); 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 ///// SIGNATURE GENERATION / VERIFICATION
generateSignature: function(keyPair, payload, nonce) { generateSignature: function(keyPair, payload, nonce) {

View File

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

View File

@ -42,24 +42,24 @@ var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups")
// ValidationAuthorityImpl represents a VA // ValidationAuthorityImpl represents a VA
type ValidationAuthorityImpl struct { type ValidationAuthorityImpl struct {
RA core.RegistrationAuthority RA core.RegistrationAuthority
log *blog.AuditLogger log *blog.AuditLogger
DNSResolver core.DNSResolver DNSResolver core.DNSResolver
IssuerDomain string IssuerDomain string
simpleHTTPPort int httpPort int
simpleHTTPSPort int httpsPort int
dvsniPort int tlsPort int
UserAgent string UserAgent string
stats statsd.Statter stats statsd.Statter
clk clock.Clock clk clock.Clock
} }
// PortConfig specifies what ports the VA should call to on the remote // PortConfig specifies what ports the VA should call to on the remote
// host when performing its checks. // host when performing its checks.
type PortConfig struct { type PortConfig struct {
SimpleHTTPPort int HTTPPort int
SimpleHTTPSPort int HTTPSPort int
DVSNIPort int TLSPort int
} }
// NewValidationAuthorityImpl constructs a new VA // NewValidationAuthorityImpl constructs a new VA
@ -67,12 +67,12 @@ func NewValidationAuthorityImpl(pc *PortConfig, stats statsd.Statter, clk clock.
logger := blog.GetAuditLogger() logger := blog.GetAuditLogger()
logger.Notice("Validation Authority Starting") logger.Notice("Validation Authority Starting")
return &ValidationAuthorityImpl{ return &ValidationAuthorityImpl{
log: logger, log: logger,
simpleHTTPPort: pc.SimpleHTTPPort, httpPort: pc.HTTPPort,
simpleHTTPSPort: pc.SimpleHTTPSPort, httpsPort: pc.HTTPSPort,
dvsniPort: pc.DVSNIPort, tlsPort: pc.TLSPort,
stats: stats, stats: stats,
clk: clk, clk: clk,
} }
} }
@ -86,6 +86,7 @@ type verificationRequestEvent struct {
Error string `json:",omitempty"` 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 { func verifyValidationJWS(validation *jose.JsonWebSignature, accountKey *jose.JsonWebKey, target map[string]interface{}) error {
if len(validation.Signatures) > 1 { if len(validation.Signatures) > 1 {
return fmt.Errorf("Too many signatures on validation JWS") 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 // resolveAndConstructDialer gets the prefered address using va.getAddr and returns
// the chosen address and dialer for that address and correct port. // the chosen address and dialer for that address and correct port.
func (va *ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort string) (dialer, *core.ProblemDetails) { func (va *ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort string) (dialer, *core.ProblemDetails) {
port := fmt.Sprintf("%d", va.simpleHTTPPort) port := fmt.Sprintf("%d", va.httpPort)
if defaultPort != "" { if defaultPort != "" {
port = defaultPort port = defaultPort
} }
@ -200,50 +201,37 @@ func (va *ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort s
// Validation methods // 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 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 host := identifier.Value
var scheme string scheme := "http"
var port int port := va.httpPort
if input.TLS == nil || (input.TLS != nil && *input.TLS) { if useTLS {
scheme = "https" scheme = "https"
port = va.simpleHTTPSPort port = va.httpsPort
} else {
scheme = "http"
port = va.simpleHTTPPort
} }
portString := fmt.Sprintf("%d", port) portString := strconv.Itoa(port)
hostPort := net.JoinHostPort(host, portString) hostPort := net.JoinHostPort(host, portString)
url := &url.URL{ url := &url.URL{
Scheme: scheme, Scheme: scheme,
Host: hostPort, Host: hostPort,
Path: fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token), Path: path,
} }
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c // 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) httpRequest, err := http.NewRequest("GET", url.String(), nil)
if err != nil { if err != nil {
challenge.Error = &core.ProblemDetails{ challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem, 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 challenge.Status = core.StatusInvalid
return challenge, err return emptyBody, challenge, err
} }
if va.UserAgent != "" { if va.UserAgent != "" {
@ -257,7 +245,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
if prob != nil { if prob != nil {
challenge.Status = core.StatusInvalid challenge.Status = core.StatusInvalid
challenge.Error = prob challenge.Error = prob
return challenge, prob return emptyBody, challenge, prob
} }
tr := &http.Transport{ tr := &http.Transport{
@ -303,7 +291,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
return err return err
} }
tr.Dial = dialer.Dial 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 return nil
} }
client := http.Client{ client := http.Client{
@ -319,7 +307,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
Detail: fmt.Sprintf("Could not connect to %s", url), Detail: fmt.Sprintf("Could not connect to %s", url),
} }
va.log.Debug(strings.Join([]string{challenge.Error.Error(), err.Error()}, ": ")) va.log.Debug(strings.Join([]string{challenge.Error.Error(), err.Error()}, ": "))
return challenge, err return emptyBody, challenge, err
} }
if httpResponse.StatusCode != 200 { if httpResponse.StatusCode != 200 {
@ -330,18 +318,107 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
url.String(), dialer.record.AddressUsed, httpResponse.StatusCode), url.String(), dialer.record.AddressUsed, httpResponse.StatusCode),
} }
err = challenge.Error err = challenge.Error
return challenge, err return emptyBody, challenge, err
} }
// Read body & test // Read body & test
body, readErr := ioutil.ReadAll(httpResponse.Body) body, err := ioutil.ReadAll(httpResponse.Body)
if readErr != nil { if err != nil {
challenge.Status = core.StatusInvalid challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{ challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem, 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 // Parse and verify JWS
@ -381,6 +458,7 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
return challenge, nil 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) { func (va *ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input challenge := input
@ -417,67 +495,82 @@ func (va *ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier,
h := sha256.New() h := sha256.New()
h.Write([]byte(encodedSignature)) h.Write([]byte(encodedSignature))
Z := hex.EncodeToString(h.Sum(nil)) 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) return va.validateTLSWithZName(identifier, challenge, ZName)
challenge.ValidationRecord = []core.ValidationRecord{ }
core.ValidationRecord{
Hostname: identifier.Value, func (va *ValidationAuthorityImpl) validateHTTP01(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
AddressesResolved: allAddrs, challenge := input
AddressUsed: addr,
}, if identifier.Type != core.IdentifierDNS {
}
if problem != nil {
challenge.Status = core.StatusInvalid 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 return challenge, challenge.Error
} }
// Make a connection with SNI = nonceName // Perform the fetch
portString := fmt.Sprintf("%d", va.dvsniPort) path := fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token)
hostPort := net.JoinHostPort(addr.String(), portString) body, challenge, err := va.fetchHTTP(identifier, path, false, challenge)
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,
})
if err != nil { 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 return challenge, err
} }
defer conn.Close()
// Check that ZName is a dNSName SAN in the server's certificate // Parse body as a key authorization object
certs := conn.ConnectionState().PeerCertificates serverKeyAuthorization, err := core.NewKeyAuthorizationFromString(string(body))
if len(certs) == 0 { 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{ challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem, Type: core.UnauthorizedProblem,
Detail: "No certs presented for DVSNI challenge", Detail: err.Error(),
}
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
} }
return challenge, err
} }
challenge.Error = &core.ProblemDetails{ // Check that the account key for this challenge is authorized by this object
Type: core.UnauthorizedProblem, if !serverKeyAuthorization.Match(challenge.Token, challenge.AccountKey) {
Detail: "Correct ZName not found for DVSNI challenge", 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 // parseHTTPConnError returns the ACME ProblemType corresponding to an error
@ -502,7 +595,7 @@ func parseHTTPConnError(err error) core.ProblemType {
return core.ConnectionProblem 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 challenge := input
if identifier.Type != core.IdentifierDNS { if identifier.Type != core.IdentifierDNS {
@ -515,24 +608,10 @@ func (va *ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, i
return challenge, challenge.Error return challenge, challenge.Error
} }
// Check that JWS body is as expected // Compute the digest of the key authorization file
// * "type" == "dvsni" h := sha256.New()
// * "token" == challenge.token h.Write([]byte(challenge.KeyAuthorization.String()))
target := map[string]interface{}{ authorizedKeysDigest := hex.EncodeToString(h.Sum(nil))
"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)
// Look for the required record in the DNS // Look for the required record in the DNS
challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, identifier.Value) 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 { for _, element := range txts {
if subtle.ConstantTimeCompare([]byte(element), []byte(encodedSignature)) == 1 { if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 {
challenge.Status = core.StatusValid challenge.Status = core.StatusValid
return challenge, nil return challenge, nil
} }
@ -583,11 +662,17 @@ func (va *ValidationAuthorityImpl) validate(authz core.Authorization, challengeI
vStart := va.clk.Now() vStart := va.clk.Now()
switch authz.Challenges[challengeIndex].Type { switch authz.Challenges[challengeIndex].Type {
case core.ChallengeTypeSimpleHTTP: 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]) authz.Challenges[challengeIndex], err = va.validateSimpleHTTP(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeDVSNI: 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]) authz.Challenges[challengeIndex], err = va.validateDvsni(authz.Identifier, authz.Challenges[challengeIndex])
case core.ChallengeTypeDNS: case core.ChallengeTypeHTTP01:
authz.Challenges[challengeIndex], err = va.validateDNS(authz.Identifier, authz.Challenges[challengeIndex]) 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) 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() var log = mocks.UseMockLog()
const expectedToken = "THETOKEN" // All paths that get assigned to tokens MUST be valid tokens
const pathWrongToken = "wrongtoken" const expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
const pathWrongToken = "i6lNAC4lOOLYCl-A08VJt9z_tKYvVk63Dumo8icsBjQ"
const path404 = "404" const path404 = "404"
const pathFound = "302" const pathFound = "GBq8SwWq3JsbREFdCamk5IX3KLsxW5ULeGs98Ajl_UM"
const pathMoved = "301" const pathMoved = "5J4FIMrWNfmvHZo-QpKZngmuhqZGwRm21-oEgUDstJM"
const pathRedirectLookup = "re-lookup"
const pathRedirectLookupInvalid = "re-lookup-invalid"
const pathRedirectPort = "port-redirect" 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 { func createValidation(token string, enableTLS bool) string {
payload, _ := json.Marshal(map[string]interface{}{ payload, _ := json.Marshal(map[string]interface{}{
"type": "simpleHttp", "type": "simpleHttp",
@ -85,6 +91,7 @@ func createValidation(token string, enableTLS bool) string {
return obj.FullSerialize() 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 { func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
m := http.NewServeMux() m := http.NewServeMux()
@ -110,22 +117,22 @@ func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
currentToken = pathFound currentToken = pathFound
} }
http.Redirect(w, r, pathMoved, 302) 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") t.Logf("SIMPLESRV: Got a wait req\n")
time.Sleep(time.Second * 3) 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") t.Logf("SIMPLESRV: Got a wait-long req\n")
time.Sleep(time.Second * 10) 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") t.Logf("SIMPLESRV: Got a redirect req to a valid hostname\n")
if currentToken == defaultToken { if currentToken == defaultToken {
currentToken = "re-lookup" currentToken = pathReLookup
} }
http.Redirect(w, r, "http://other.valid/path", 302) 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") t.Logf("SIMPLESRV: Got a redirect req to a invalid hostname\n")
http.Redirect(w, r, "http://invalid.invalid/path", 302) 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") t.Logf("SIMPLESRV: Got a loop req\n")
http.Redirect(w, r, r.URL.String(), 301) http.Redirect(w, r, r.URL.String(), 301)
} else if strings.HasSuffix(r.URL.Path, pathRedirectPort) { } 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 return server
} }
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server { func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature) encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature)
h := sha256.New() h := sha256.New()
@ -221,17 +229,7 @@ func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
return hs return hs
} }
func brokenTLSSrv() *httptest.Server { // TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
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 TestSimpleHttpTLS(t *testing.T) { func TestSimpleHttpTLS(t *testing.T) {
chall := core.Challenge{ chall := core.Challenge{
Type: core.ChallengeTypeSimpleHTTP, Type: core.ChallengeTypeSimpleHTTP,
@ -246,18 +244,19 @@ func TestSimpleHttpTLS(t *testing.T) {
port, err := getPort(hs) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPSPort: port}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{HTTPSPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
log.Clear() log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall) finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Error validating simpleHttp") 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, len(logs), 1)
test.AssertEquals(t, logs[0].Priority, syslog.LOG_NOTICE) 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) { func TestSimpleHttp(t *testing.T) {
tls := false tls := false
chall := core.Challenge{ chall := core.Challenge{
@ -288,16 +287,16 @@ func TestSimpleHttp(t *testing.T) {
badPort = goodPort - 1 badPort = goodPort - 1
} }
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: badPort}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: badPort}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
invalidChall, err := va.validateSimpleHTTP(ident, chall) invalidChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?") test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem) test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
va = NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: goodPort}, stats, clock.Default()) va = NewValidationAuthorityImpl(&PortConfig{HTTPPort: goodPort}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
log.Clear() log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall) finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
@ -327,15 +326,15 @@ func TestSimpleHttp(t *testing.T) {
finChall, err = va.validateSimpleHTTP(ident, chall) finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Failed to follow 301 redirect") 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() log.Clear()
chall.Token = pathFound chall.Token = pathFound
finChall, err = va.validateSimpleHTTP(ident, chall) finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "Failed to follow 302 redirect") 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 ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 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"} ipIdentifier := core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}
invalidChall, err = va.validateSimpleHTTP(ipIdentifier, chall) invalidChall, err = va.validateSimpleHTTP(ipIdentifier, chall)
@ -360,6 +359,7 @@ func TestSimpleHttp(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem) test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
} }
// TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
func TestSimpleHttpRedirectLookup(t *testing.T) { func TestSimpleHttpRedirectLookup(t *testing.T) {
tls := false tls := false
chall := core.Challenge{ chall := core.Challenge{
@ -374,15 +374,15 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
port, err := getPort(hs) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: port}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
log.Clear() log.Clear()
chall.Token = pathMoved chall.Token = pathMoved
finChall, err := va.validateSimpleHTTP(ident, chall) finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token) 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) test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 2)
log.Clear() log.Clear()
@ -390,12 +390,12 @@ func TestSimpleHttpRedirectLookup(t *testing.T) {
finChall, err = va.validateSimpleHTTP(ident, chall) finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token) 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 ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
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\]`)), 3) test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 3)
log.Clear() log.Clear()
chall.Token = pathRedirectLookupInvalid chall.Token = pathReLookupInvalid
finChall, err = va.validateSimpleHTTP(ident, chall) finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusInvalid) test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token) 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) test.AssertEquals(t, len(log.GetAllMatching(`No IPv4 addresses found for invalid.invalid`)), 1)
log.Clear() log.Clear()
chall.Token = pathRedirectLookup chall.Token = pathReLookup
finChall, err = va.validateSimpleHTTP(ident, chall) finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid) test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token) 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 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) 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) 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) { func TestSimpleHttpRedirectLoop(t *testing.T) {
tls := false tls := false
chall := core.Challenge{ chall := core.Challenge{
@ -435,8 +436,8 @@ func TestSimpleHttpRedirectLoop(t *testing.T) {
port, err := getPort(hs) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: port}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
log.Clear() log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall) finChall, err := va.validateSimpleHTTP(ident, chall)
@ -445,22 +446,7 @@ func TestSimpleHttpRedirectLoop(t *testing.T) {
fmt.Println(finChall) fmt.Println(finChall)
} }
func getPort(hs *httptest.Server) (int, error) { // TODO(https://github.com/letsencrypt/boulder/issues/894): Remove this method
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 TestDvsni(t *testing.T) { func TestDvsni(t *testing.T) {
chall := createChallenge(core.ChallengeTypeDVSNI) chall := createChallenge(core.ChallengeTypeDVSNI)
@ -469,9 +455,9 @@ func TestDvsni(t *testing.T) {
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() 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() log.Clear()
finChall, err := va.validateDvsni(ident, chall) finChall, err := va.validateDvsni(ident, chall)
@ -523,15 +509,15 @@ func TestDvsni(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem) test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
} }
func TestTLSError(t *testing.T) { func TestDVSNIWithTLSError(t *testing.T) {
chall := createChallenge(core.ChallengeTypeDVSNI) chall := createChallenge(core.ChallengeTypeDVSNI)
hs := brokenTLSSrv() hs := brokenTLSSrv()
port, err := getPort(hs) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() 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{}
invalidChall, err := va.validateDvsni(ident, chall) invalidChall, err := va.validateDvsni(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid) test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
@ -539,19 +525,403 @@ func TestTLSError(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.TLSProblem) test.AssertEquals(t, invalidChall.Error.Type, core.TLSProblem)
} }
func TestValidateHTTP(t *testing.T) { func httpSrv(t *testing.T, token string) *httptest.Server {
tls := false m := http.NewServeMux()
challHTTP := core.SimpleHTTPChallenge()
challHTTP.TLS = &tls
challHTTP.ValidationRecord = []core.ValidationRecord{}
challHTTP.AccountKey = accountKey
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) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{SimpleHTTPPort: port}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{HTTPPort: port}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} 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{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
@ -561,14 +931,14 @@ func TestValidateHTTP(t *testing.T) {
ID: core.NewToken(), ID: core.NewToken(),
RegistrationID: 1, RegistrationID: 1,
Identifier: ident, Identifier: ident,
Challenges: []core.Challenge{challHTTP}, Challenges: []core.Challenge{chall},
} }
va.validate(authz, 0) va.validate(authz, 0)
test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status) 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 { func createChallenge(challengeType string) core.Challenge {
chall := core.Challenge{ chall := core.Challenge{
Type: challengeType, Type: challengeType,
@ -578,27 +948,45 @@ func createChallenge(challengeType string) core.Challenge {
AccountKey: accountKey, 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{}{ validationPayload, _ := json.Marshal(map[string]interface{}{
"type": chall.Type, "type": chall.Type,
"token": chall.Token, "token": chall.Token,
}) })
signer, _ := jose.NewSigner(jose.RS256, &TheKey) signer, _ := jose.NewSigner(jose.RS256, &TheKey)
chall.Validation, _ = signer.Sign(validationPayload, "") chall.Validation, _ = signer.Sign(validationPayload, "")
return chall return chall
} }
func TestValidateDvsni(t *testing.T) { // setChallengeToken sets the token value both in the Token field and
chall := createChallenge(core.ChallengeTypeDVSNI) // in the serialized KeyAuthorization object.
hs := dvsniSrv(t, chall) 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() defer hs.Close()
port, err := getPort(hs) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
stats, _ := statsd.NewNoopClient() 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{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
@ -613,14 +1001,14 @@ func TestValidateDvsni(t *testing.T) {
test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status) test.AssertEquals(t, core.StatusValid, mockRA.lastAuthz.Challenges[0].Status)
} }
func TestValidateDvsniNotSane(t *testing.T) { func TestValidateTLSSNINotSane(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) // no calls made va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) // no calls made
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
chall := createChallenge(core.ChallengeTypeDVSNI) chall := createChallenge(core.ChallengeTypeTLSSNI01)
chall.Token = "not sane" chall.Token = "not sane"
@ -638,20 +1026,20 @@ func TestValidateDvsniNotSane(t *testing.T) {
func TestUpdateValidations(t *testing.T) { func TestUpdateValidations(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
tls := false chall := core.HTTPChallenge01(accountKey)
challHTTP := core.SimpleHTTPChallenge() chall.ValidationRecord = []core.ValidationRecord{}
challHTTP.TLS = &tls err := setChallengeToken(&chall, core.NewToken())
challHTTP.ValidationRecord = []core.ValidationRecord{} test.AssertNotError(t, err, "Failed to complete HTTP challenge")
var authz = core.Authorization{ var authz = core.Authorization{
ID: core.NewToken(), ID: core.NewToken(),
RegistrationID: 1, RegistrationID: 1,
Identifier: ident, Identifier: ident,
Challenges: []core.Challenge{challHTTP}, Challenges: []core.Challenge{chall},
} }
started := time.Now() started := time.Now()
@ -694,7 +1082,7 @@ func TestCAAChecking(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
va.IssuerDomain = "letsencrypt.org" va.IssuerDomain = "letsencrypt.org"
for _, caaTest := range tests { for _, caaTest := range tests {
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain}) 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) { func TestDNSValidationFailure(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
chalDNS := createChallenge(core.ChallengeTypeDNS) chalDNS := createChallenge(core.ChallengeTypeDNS01)
var authz = core.Authorization{ var authz = core.Authorization{
ID: core.NewToken(), ID: core.NewToken(),
@ -753,7 +1141,7 @@ func TestDNSValidationInvalid(t *testing.T) {
Value: "790DB180-A274-47A4-855F-31C428CB1072", Value: "790DB180-A274-47A4-855F-31C428CB1072",
} }
chalDNS := core.DNSChallenge() chalDNS := core.DNSChallenge01(accountKey)
var authz = core.Authorization{ var authz = core.Authorization{
ID: core.NewToken(), ID: core.NewToken(),
@ -764,7 +1152,7 @@ func TestDNSValidationInvalid(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
@ -778,17 +1166,17 @@ func TestDNSValidationInvalid(t *testing.T) {
func TestDNSValidationNotSane(t *testing.T) { func TestDNSValidationNotSane(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
chal0 := core.DNSChallenge() chal0 := core.DNSChallenge01(accountKey)
chal0.Token = "" chal0.Token = ""
chal1 := core.DNSChallenge() chal1 := core.DNSChallenge01(accountKey)
chal1.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_" chal1.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_"
chal2 := core.DNSChallenge() chal2 := core.DNSChallenge01(accountKey)
chal2.TLS = new(bool) chal2.TLS = new(bool)
*chal2.TLS = true *chal2.TLS = true
@ -809,11 +1197,11 @@ func TestDNSValidationNotSane(t *testing.T) {
func TestDNSValidationServFail(t *testing.T) { func TestDNSValidationServFail(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
chalDNS := createChallenge(core.ChallengeTypeDNS) chalDNS := createChallenge(core.ChallengeTypeDNS01)
badIdent := core.AcmeIdentifier{ badIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS, Type: core.IdentifierDNS,
@ -839,7 +1227,7 @@ func TestDNSValidationNoServer(t *testing.T) {
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
chalDNS := createChallenge(core.ChallengeTypeDNS) chalDNS := createChallenge(core.ChallengeTypeDNS01)
var authz = core.Authorization{ var authz = core.Authorization{
ID: core.NewToken(), ID: core.NewToken(),
@ -860,11 +1248,11 @@ func TestDNSValidationNoServer(t *testing.T) {
func TestDNSValidationLive(t *testing.T) { func TestDNSValidationLive(t *testing.T) {
stats, _ := statsd.NewNoopClient() stats, _ := statsd.NewNoopClient()
va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default()) va := NewValidationAuthorityImpl(&PortConfig{}, stats, clock.Default())
va.DNSResolver = &mocks.MockDNS{} va.DNSResolver = &mocks.DNSResolver{}
mockRA := &MockRegistrationAuthority{} mockRA := &MockRegistrationAuthority{}
va.RA = mockRA va.RA = mockRA
goodChalDNS := core.DNSChallenge() goodChalDNS := core.DNSChallenge01(accountKey)
// This token is set at _acme-challenge.good.bin.coffee // This token is set at _acme-challenge.good.bin.coffee
goodChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w" goodChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w"
@ -891,7 +1279,7 @@ func TestDNSValidationLive(t *testing.T) {
t.Logf("TestDNSValidationLive on Good did not succeed.") 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 // This token is NOT set at _acme-challenge.bad.bin.coffee
badChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w" badChalDNS.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_w"

View File

@ -38,10 +38,14 @@ const (
IssuerPath = "/acme/issuer-cert" IssuerPath = "/acme/issuer-cert"
BuildIDPath = "/build" BuildIDPath = "/build"
// Not included in net/http // StatusRateLimited is not in net/http
StatusRateLimited = 429 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 { type WebFrontEndImpl struct {
RA core.RegistrationAuthority RA core.RegistrationAuthority
SA core.StorageGetter SA core.StorageGetter
@ -113,7 +117,7 @@ func statusCodeFromError(err interface{}) int {
type requestEvent struct { type requestEvent struct {
ID string `json:",omitempty"` ID string `json:",omitempty"`
RealIP string `json:",omitempty"` RealIP string `json:",omitempty"`
ForwardedFor string `json:",omitempty"` ClientAddr string `json:",omitempty"`
Endpoint string `json:",omitempty"` Endpoint string `json:",omitempty"`
Method string `json:",omitempty"` Method string `json:",omitempty"`
RequestTime time.Time `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)) 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) { func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Type", "application/json") response.Header().Set("Content-Type", "application/json")
response.Write(wfe.DirectoryJSON) 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 // Use an explicitly typed variable. Otherwise `go vet' incorrectly complains
// that reg.ID is a string being passed to %d. // that reg.ID is a string being passed to %d.
var id int64 = reg.ID regURL := fmt.Sprintf("%s%d", wfe.RegBase, reg.ID)
regURL := fmt.Sprintf("%s%d", wfe.RegBase, id)
responseBody, err := json.Marshal(reg) responseBody, err := json.Marshal(reg)
if err != nil { if err != nil {
logEvent.Error = err.Error() 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 { var csrLog = struct {
RemoteAddr string ClientAddr string
CsrBase64 []byte CsrBase64 []byte
Registration core.Registration Registration core.Registration
}{ }{
RemoteAddr: remoteAddr, ClientAddr: getClientAddr(request),
CsrBase64: cr.Bytes, CsrBase64: cr.Bytes,
Registration: registration, Registration: registration,
} }
@ -778,7 +783,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
wfe.sendError(response, "Error unmarshaling certificate request", err, http.StatusBadRequest) wfe.sendError(response, "Error unmarshaling certificate request", err, http.StatusBadRequest)
return 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 // 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 // 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 // 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( func (wfe *WebFrontEndImpl) Challenge(
response http.ResponseWriter, response http.ResponseWriter,
request *http.Request) { request *http.Request) {
@ -1298,15 +1305,26 @@ func (wfe *WebFrontEndImpl) logRequestDetails(logEvent *requestEvent) {
func (wfe *WebFrontEndImpl) populateRequestEvent(request *http.Request) (logEvent requestEvent) { func (wfe *WebFrontEndImpl) populateRequestEvent(request *http.Request) (logEvent requestEvent) {
logEvent = requestEvent{ logEvent = requestEvent{
ID: core.NewToken(), ID: core.NewToken(),
RealIP: request.Header.Get("X-Real-IP"), RealIP: request.Header.Get("X-Real-IP"),
ForwardedFor: request.Header.Get("X-Forwarded-For"), ClientAddr: getClientAddr(request),
Method: request.Method, Method: request.Method,
RequestTime: time.Now(), RequestTime: time.Now(),
Extra: make(map[string]interface{}, 0), Extra: make(map[string]interface{}, 0),
} }
if request.URL != nil { if request.URL != nil {
logEvent.Endpoint = request.URL.String() logEvent.Endpoint = request.URL.String()
} }
return 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/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/jmhodges/clock"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/cmd" "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/core"
"github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/ra" "github.com/letsencrypt/boulder/ra"
@ -172,7 +171,7 @@ func (ca *MockCA) RevokeCertificate(serial string, reasonCode core.RevocationCod
type MockPA struct{} 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 return
} }
@ -214,7 +213,7 @@ func setupWFE(t *testing.T) WebFrontEndImpl {
wfe.log.SyslogWriter = mocks.NewSyslogWriter() wfe.log.SyslogWriter = mocks.NewSyslogWriter()
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient() wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
@ -534,7 +533,7 @@ func TestIssueCertificate(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
mux, err := wfe.Handler() mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers") 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 // 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 // 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. // authorized, etc.
stats, _ := statsd.NewNoopClient(nil) stats, _ := statsd.NewNoopClient(nil)
ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log, stats, cmd.RateLimitConfig{}) ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log, stats, cmd.RateLimitConfig{})
ra.SA = &mocks.MockSA{} ra.SA = &mocks.StorageAuthority{}
ra.CA = &MockCA{} ra.CA = &MockCA{}
ra.PA = &MockPA{} ra.PA = &MockPA{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
wfe.RA = &ra wfe.RA = &ra
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
@ -671,7 +670,7 @@ func TestChallenge(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
var key jose.JsonWebKey var key jose.JsonWebKey
@ -707,7 +706,7 @@ func TestNewRegistration(t *testing.T) {
test.AssertNotError(t, err, "Problem setting up HTTP handlers") test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient() wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
@ -879,7 +878,7 @@ func makeRevokeRequestJSON() ([]byte, error) {
// registration when GetRegistrationByKey is called, and we want to get a // registration when GetRegistrationByKey is called, and we want to get a
// NoSuchRegistrationError for tests that pass regCheck = false to verifyPOST. // NoSuchRegistrationError for tests that pass regCheck = false to verifyPOST.
type mockSANoSuchRegistration struct { type mockSANoSuchRegistration struct {
mocks.MockSA mocks.StorageAuthority
} }
func (msa mockSANoSuchRegistration) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) { 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") test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.SA = &mockSANoSuchRegistration{mocks.MockSA{}} wfe.SA = &mockSANoSuchRegistration{mocks.StorageAuthority{}}
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
nonce, err := wfe.nonceService.Nonce() nonce, err := wfe.nonceService.Nonce()
@ -992,7 +991,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mockSANoSuchRegistration{mocks.MockSA{}} wfe.SA = &mockSANoSuchRegistration{mocks.StorageAuthority{}}
wfe.stats, _ = statsd.NewNoopClient() wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
@ -1013,7 +1012,7 @@ func TestAuthorization(t *testing.T) {
test.AssertNotError(t, err, "Problem setting up HTTP handlers") test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient() wfe.stats, _ = statsd.NewNoopClient()
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
@ -1101,7 +1100,7 @@ func TestRegistration(t *testing.T) {
test.AssertNotError(t, err, "Problem setting up HTTP handlers") test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient() wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
@ -1192,7 +1191,7 @@ func TestTermsRedirect(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{} wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
wfe.stats, _ = statsd.NewNoopClient() wfe.stats, _ = statsd.NewNoopClient()
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
@ -1228,59 +1227,64 @@ func TestGetCertificate(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.CertCacheDuration = time.Second * 10 wfe.CertCacheDuration = time.Second * 10
wfe.CertNoCacheExpirationWindow = time.Hour * 24 * 7 wfe.CertNoCacheExpirationWindow = time.Hour * 24 * 7
wfe.SA = &mocks.MockSA{} wfe.SA = &mocks.StorageAuthority{}
certPemBytes, _ := ioutil.ReadFile("test/178.crt") certPemBytes, _ := ioutil.ReadFile("test/178.crt")
certBlock, _ := pem.Decode(certPemBytes) certBlock, _ := pem.Decode(certPemBytes)
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
mockLog := wfe.log.SyslogWriter.(*mocks.SyslogWriter)
mockLog.Clear()
// Valid serial, cached // Valid serial, cached
path, _ := url.Parse("/acme/cert/0000000000000000000000000000000000b2") req, _ := http.NewRequest("GET", "/acme/cert/0000000000000000000000000000000000b2", nil)
wfe.Certificate(responseWriter, &http.Request{ req.RemoteAddr = "192.168.0.1"
Method: "GET", wfe.Certificate(responseWriter, req)
URL: path,
})
test.AssertEquals(t, responseWriter.Code, 200) test.AssertEquals(t, responseWriter.Code, 200)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10") test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/pkix-cert") 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") 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 // Unused serial, no cache
mockLog.Clear()
responseWriter = httptest.NewRecorder() responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/0000000000000000000000000000000000ff") req, _ = http.NewRequest("GET", "/acme/cert/0000000000000000000000000000000000ff", nil)
wfe.Certificate(responseWriter, &http.Request{ req.RemoteAddr = "192.168.0.1"
Method: "GET", req.Header.Set("X-Forwarded-For", "192.168.99.99")
URL: path, 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.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache") 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"}`) test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
// Invalid serial, no cache // Invalid serial, no cache
responseWriter = httptest.NewRecorder() responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/nothex") req, _ = http.NewRequest("GET", "/acme/cert/00000000000000", nil)
wfe.Certificate(responseWriter, &http.Request{ wfe.Certificate(responseWriter, req)
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,
})
test.AssertEquals(t, responseWriter.Code, 404) test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache") 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"}`) 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=") matches := mockLog.GetAllMatching("^\\[AUDIT\\] Certificate request JSON=")
test.Assert(t, len(matches) == 1, test.Assert(t, len(matches) == 1,
fmt.Sprintf("Incorrect number of certificate request log entries: %d", 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) err := json.Unmarshal([]byte(certificateRequestJSON), &certificateRequest)
test.AssertNotError(t, err, "Unable to parse certificateRequest") test.AssertNotError(t, err, "Unable to parse certificateRequest")
mockSA := mocks.MockSA{} mockSA := mocks.StorageAuthority{}
reg, err := mockSA.GetRegistration(789) reg, err := mockSA.GetRegistration(789)
test.AssertNotError(t, err, "Unable to get registration") 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") matches := mockLog.GetAllMatching("Certificate request")
test.Assert(t, len(matches) == 1, test.Assert(t, len(matches) == 1,
"Incorrect number of certificate request log entries") "Incorrect number of certificate request log entries")
test.AssertEquals(t, matches[0].Priority, syslog.LOG_NOTICE) 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) { func TestLengthRequired(t *testing.T) {
@ -1325,7 +1334,7 @@ func TestLengthRequired(t *testing.T) {
} }
type mockSADifferentStoredKey struct { type mockSADifferentStoredKey struct {
mocks.MockSA mocks.StorageAuthority
} }
func (sa mockSADifferentStoredKey) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) { 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) { func TestVerifyPOSTUsesStoredKey(t *testing.T) {
wfe := setupWFE(t) wfe := setupWFE(t)
wfe.SA = &mockSADifferentStoredKey{mocks.MockSA{}} wfe.SA = &mockSADifferentStoredKey{mocks.StorageAuthority{}}
// signRequest signs with test1Key, but our special mock returns a // signRequest signs with test1Key, but our special mock returns a
// registration with test2Key // registration with test2Key
_, _, _, err := wfe.verifyPOST(makePostRequest(signRequest(t, `{"resource":"foo"}`, &wfe.nonceService)), true, "foo") _, _, _, err := wfe.verifyPOST(makePostRequest(signRequest(t, `{"resource":"foo"}`, &wfe.nonceService)), true, "foo")