Merge master

This commit is contained in:
Roland Shoemaker 2015-06-25 15:59:59 -07:00
commit 12589834a3
19 changed files with 505 additions and 214 deletions

View File

@ -61,6 +61,22 @@ type PKCS11Config struct {
Label string
}
// This map is used to detect algorithms in crypto/x509 that
// are no longer considered sufficiently strong.
// * No MD2, MD5, or SHA-1
// * No DSA
//
// SHA1WithRSA is allowed because there's still a fair bit of it
// out there, but we should try to remove it soon.
var badSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{
x509.UnknownSignatureAlgorithm: true,
x509.MD2WithRSA: true,
x509.MD5WithRSA: true,
x509.DSAWithSHA1: true,
x509.DSAWithSHA256: true,
x509.ECDSAWithSHA1: true,
}
// CertificateAuthorityImpl represents a CA that signs certificates, CRLs, and
// OCSP responses.
type CertificateAuthorityImpl struct {
@ -277,6 +293,12 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
ca.log.AuditErr(err)
return emptyCert, err
}
if badSignatureAlgorithms[csr.SignatureAlgorithm] {
err = fmt.Errorf("Invalid signature algorithm in CSR")
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
ca.log.AuditErr(err)
return emptyCert, err
}
// Pull hostnames from CSR
// Authorization is checked by the RA

View File

@ -11,7 +11,6 @@ import (
"encoding/asn1"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"testing"
"time"
@ -275,6 +274,59 @@ var TooManyNameCSRhex = "308202aa30820192020100300d310b3009060355040613025553308
"9c470101aed4852cd7b3a0a5a489b264779118b30d51907dc745879a821b" +
"85f19a24d20dc5b48141eff276aa73bb41141305905b0223ded7"
// CSR generated by Go:
// * Random public key -- 512 bits long
// * CN = (none)
// * DNSNames = not-example.com, www.not-example.com, mail.not-example.com
var ShortKeyCSRhex = "3082011f3081ca020100300d310b30090603550406130255" +
"53305c300d06092a864886f70d0101010500034b00304802" +
"4100ca8ff48b32197226bbe6398b6ba62dd4f09be06dc742" +
"4b0c6321323d90c58e93cad2b3c91b5809493836f110dfc6" +
"bbb3eca5575e6416a3a8189e782f41a29b990203010001a0" +
"58305606092a864886f70d01090e3149304730450603551d" +
"11043e303c820f6e6f742d6578616d706c652e636f6d8213" +
"7777772e6e6f742d6578616d706c652e636f6d82146d6169" +
"6c2e6e6f742d6578616d706c652e636f6d300d06092a8648" +
"86f70d0101050500034100bfdc34a0a728918f4221413631" +
"a70656db80ddb1b8126a02e6ca92c48301c350f29b6049b3" +
"c4d70bf22ac273c951e60dd22f6f427b8d234e85f6881fba" +
"35c288"
// CSR generated by Go:
// * Random public key
// * CN = (none)
// * DNSNames = not-example.com, www.not-example.com, mail.not-example.com
// * Signature algorithm: SHA1WithRSA
var BadAlgorithmCSRhex = "308202aa30820192020100300d310b300906035504061302" +
"555330820122300d06092a864886f70d0101010500038201" +
"0f003082010a02820101009f70f8d60b07f24b6960e18da8" +
"aabdc993c583a6c43c81ae256a1d4328146f3e907fb65dc4" +
"f30cb27c96d020e55bf116ef33f3696d2d957b9b408d6762" +
"2fe562311c96023100985ed8bc10929e7c552d7674027b63" +
"3973ede006b1ba338f9f4b7a88b4ea768c70632dde0f202a" +
"f59d41bf9aaa841e022735cfb2822a919b2b9ada07bdd569" +
"347894f1aa4908ee86d2019eb8980e819af20b5084eb11f7" +
"2f4f51e77c99d09e556ae84a720fa728962c5f7db176ce0a" +
"27b824fc63849ad5493ba790cd8f0c0aedf22d7979a95fce" +
"f4dfa552f8f85e15f5e7e427002b56f03f040e7cd43145d7" +
"7732ef9c4568b0f22aa91ca1e5a133f862939ed14c1859a1" +
"fdd36f0203010001a058305606092a864886f70d01090e31" +
"49304730450603551d11043e303c820f6e6f742d6578616d" +
"706c652e636f6d82137777772e6e6f742d6578616d706c65" +
"2e636f6d82146d61696c2e6e6f742d6578616d706c652e63" +
"6f6d300d06092a864886f70d0101050500038201010056cf" +
"65a690ef9d279caa0b5e44c51a1f97631c43e9f0c67fd329" +
"6b0d33acf06a695130fbdb8842a40d690549ab05f44b1738" +
"e8419b395563426d76cc5611d17ade13c96c92496a2ed008" +
"a78687f25c892af274afd9518540300edd160dc47887b402" +
"a0705b156e9b623f47a3bdaea93b84a4cf873ac1e9861b67" +
"f742024eff40c7c8e33243a953e9874e678c3f4cd24b4699" +
"28214425ef7b9181d8b10b7f07f557bef7c4b22689be4b2a" +
"42a0b6f0aaefa4dc8913338785e7f8fc2b1b95c4d189d10f" +
"72dbe575cdd6985e2d6eb0d0cbe962de0986cbcc8ca30dc8" +
"fa254808b0ee8946253830c087da95404181cb88ec59d399" +
"b4b3fcc21bbb1c9f3e1b29fd576a"
// Times for validity checking
var FarFuture = time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)
var FarPast = time.Date(1950, 1, 1, 0, 0, 0, 0, time.UTC)
@ -546,13 +598,22 @@ func TestShortKey(t *testing.T) {
ca.SA = storageAuthority
ca.MaxKeySize = 4096
csrDER, err := ioutil.ReadFile("shortkey-csr.der")
if err != nil {
t.Errorf("Failed to read shortkey-csr.der")
}
// Test that the CA rejects CSRs that would expire after the intermediate cert
csrDER, _ := hex.DecodeString(ShortKeyCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarFuture)
if err == nil {
t.Errorf("CA improperly created a certificate with short key.")
}
test.Assert(t, err != nil, "Issued a certificate with too short a key.")
}
func TestRejectBadAlgorithm(t *testing.T) {
cadb, storageAuthority, caConfig := setup(t)
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile)
ca.SA = storageAuthority
ca.MaxKeySize = 4096
// Test that the CA rejects CSRs that would expire after the intermediate cert
csrDER, _ := hex.DecodeString(BadAlgorithmCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarFuture)
test.Assert(t, err != nil, "Issued a certificate based on a CSR with a weak algorithm.")
}

Binary file not shown.

View File

@ -40,6 +40,7 @@ func main() {
dnsTimeout, err := time.ParseDuration(c.VA.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse DNS timeout")
vai.DNSResolver = core.NewDNSResolver(dnsTimeout, []string{c.VA.DNSResolver})
vai.UserAgent = c.VA.UserAgent
for {
ch, err := cmd.AmqpChannel(c)

View File

@ -55,7 +55,8 @@ func main() {
blog.SetAuditLogger(auditlogger)
wfe := wfe.NewWebFrontEndImpl()
wfe, err := wfe.NewWebFrontEndImpl()
cmd.FailOnError(err, "Unable to create WFE")
rac, sac, closeChan := setupWFE(c)
wfe.RA = &rac
wfe.SA = &sac

View File

@ -42,7 +42,8 @@ func main() {
go cmd.ProfileCmd("Monolith", stats)
// Create the components
wfei := wfe.NewWebFrontEndImpl()
wfei, err := wfe.NewWebFrontEndImpl()
cmd.FailOnError(err, "Unable to create WFE")
sa, err := sa.NewSQLStorageAuthority(c.SA.DBDriver, c.SA.DBName)
cmd.FailOnError(err, "Unable to create SA")
sa.SetSQLDebug(c.SQL.SQLDebug)
@ -53,6 +54,7 @@ func main() {
dnsTimeout, err := time.ParseDuration(c.VA.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse DNS timeout")
va.DNSResolver = core.NewDNSResolver(dnsTimeout, []string{c.VA.DNSResolver})
va.UserAgent = c.VA.UserAgent
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(c.CA.DBDriver, c.CA.DBName)
cmd.FailOnError(err, "Failed to create CA database")

View File

@ -58,7 +58,7 @@ type Config struct {
SA Queue
CA Queue
OCSP Queue
SSL *SSLConfig
TLS *TLSConfig
}
WFE struct {
@ -76,6 +76,7 @@ type Config struct {
VA struct {
DNSResolver string
DNSTimeout string
UserAgent string
}
SQL struct {
@ -130,11 +131,11 @@ type Config struct {
SubscriberAgreementURL string
}
// SSLConfig reprents certificates and a key for authenticated TLS.
type SSLConfig struct {
CertFile string
KeyFile string
CACertFile *string // Optional
// TLSConfig reprents certificates and a key for authenticated TLS.
type TLSConfig struct {
CertFile *string
KeyFile *string
CACertFile *string
}
// Queue describes a queue name
@ -209,43 +210,61 @@ func FailOnError(err error, msg string) {
// more aggressive error dropping
func AmqpChannel(conf Config) (*amqp.Channel, error) {
var conn *amqp.Connection
var err error
log := blog.GetAuditLogger()
if conf.AMQP.TLS == nil {
// Configuration did not specify TLS options, but Dial will
// use TLS anyway if the URL scheme is "amqps"
conn, err = amqp.Dial(conf.AMQP.Server)
} else {
// They provided TLS options, so let's load them.
log.Info("AMQPS: Loading TLS Options.")
if conf.AMQP.SSL != nil {
if strings.HasPrefix(conf.AMQP.Server, "amqps") == false {
err := fmt.Errorf("SSL configuration provided, but not using an AMQPS URL")
err = fmt.Errorf("AMQPS: TLS configuration provided, but not using an AMQPS URL")
return nil, err
}
cfg := new(tls.Config)
cert, err := tls.LoadX509KeyPair(conf.AMQP.SSL.CertFile, conf.AMQP.SSL.KeyFile)
if err != nil {
err = fmt.Errorf("Could not load Client Certificates: %s", err)
return nil, err
}
cfg.Certificates = append(cfg.Certificates, cert)
// If the configuration specified a certificate (or key), load them
if conf.AMQP.TLS.CertFile != nil || conf.AMQP.TLS.KeyFile != nil {
// But they have to give both.
if conf.AMQP.TLS.CertFile == nil || conf.AMQP.TLS.KeyFile == nil {
err = fmt.Errorf("AMQPS: You must set both of the configuration values AMQP.TLS.KeyFile and AMQP.TLS.CertFile.")
return nil, err
}
if conf.AMQP.SSL.CACertFile != nil {
cert, err := tls.LoadX509KeyPair(*conf.AMQP.TLS.CertFile, *conf.AMQP.TLS.KeyFile)
if err != nil {
err = fmt.Errorf("AMQPS: Could not load Client Certificate or Key: %s", err)
return nil, err
}
log.Info("AMQPS: Configured client certificate for AMQPS.")
cfg.Certificates = append(cfg.Certificates, cert)
}
// If the configuration specified a CA certificate, make it the only
// available root.
if conf.AMQP.TLS.CACertFile != nil {
cfg.RootCAs = x509.NewCertPool()
ca, err := ioutil.ReadFile(*conf.AMQP.SSL.CACertFile)
ca, err := ioutil.ReadFile(*conf.AMQP.TLS.CACertFile)
if err != nil {
err = fmt.Errorf("Could not load CA Certificate: %s", err)
err = fmt.Errorf("AMQPS: Could not load CA Certificate: %s", err)
return nil, err
}
cfg.RootCAs.AppendCertsFromPEM(ca)
log.Info("AMQPS: Configured CA certificate for AMQPS.")
}
conn, err = amqp.DialTLS(conf.AMQP.Server, cfg)
if err != nil {
return nil, err
}
return conn.Channel()
}
// Configuration did not specify SSL options
conn, err := amqp.Dial(conf.AMQP.Server)
if err != nil {
return nil, err
}

View File

@ -26,10 +26,11 @@ type NonceService struct {
}
// NewNonceService constructs a NonceService with defaults
func NewNonceService() NonceService {
// XXX ignoring possible error due to entropy starvation
func NewNonceService() (NonceService, error) {
key := make([]byte, 16)
rand.Read(key)
if _, err := rand.Read(key); err != nil {
return NonceService{}, err
}
// It is safe to ignore these errors because they only happen
// on key size and block size mismatches.
@ -42,17 +43,18 @@ func NewNonceService() NonceService {
used: make(map[int64]bool, MaxUsed),
gcm: gcm,
maxUsed: MaxUsed,
}
}, nil
}
func (ns NonceService) encrypt(counter int64) string {
func (ns NonceService) encrypt(counter int64) (string, error) {
// Generate a nonce with upper 4 bytes zero
// XXX ignoring possible error due to entropy starvation
nonce := make([]byte, 12)
for i := 0; i < 4; i++ {
nonce[i] = 0
}
rand.Read(nonce[4:])
if _, err := rand.Read(nonce[4:]); err != nil {
return "", err
}
// Encode counter to plaintext
pt := make([]byte, 8)
@ -65,7 +67,7 @@ func (ns NonceService) encrypt(counter int64) string {
ct := ns.gcm.Seal(nil, nonce, pt, nil)
copy(ret, nonce[4:])
copy(ret[8:], ct)
return B64enc(ret)
return B64enc(ret), nil
}
func (ns NonceService) decrypt(nonce string) (int64, error) {
@ -91,7 +93,7 @@ func (ns NonceService) decrypt(nonce string) (int64, error) {
}
// Nonce provides a new Nonce.
func (ns *NonceService) Nonce() string {
func (ns *NonceService) Nonce() (string, error) {
ns.latest++
return ns.encrypt(ns.latest)
}

View File

@ -11,48 +11,65 @@ import (
)
func TestValidNonce(t *testing.T) {
ns := NewNonceService()
n := ns.Nonce()
ns, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, ns.Valid(n), "Did not recognize fresh nonce")
}
func TestAlreadyUsed(t *testing.T) {
ns := NewNonceService()
n := ns.Nonce()
ns, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, ns.Valid(n), "Did not recognize fresh nonce")
test.Assert(t, !ns.Valid(n), "Recognized the same nonce twice")
}
func TestRejectMalformed(t *testing.T) {
ns := NewNonceService()
n := ns.Nonce()
ns, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, !ns.Valid("asdf"+n), "Accepted an invalid nonce")
}
func TestRejectUnknown(t *testing.T) {
ns1 := NewNonceService()
ns2 := NewNonceService()
n := ns1.Nonce()
ns1, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
ns2, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns1.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, !ns2.Valid(n), "Accepted a foreign nonce")
}
func TestRejectTooLate(t *testing.T) {
ns := NewNonceService()
ns, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
ns.latest = 2
n := ns.Nonce()
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
ns.latest = 1
test.Assert(t, !ns.Valid(n), "Accepted a nonce with a too-high counter")
}
func TestRejectTooEarly(t *testing.T) {
ns := NewNonceService()
ns, err := NewNonceService()
test.AssertNotError(t, err, "Could not create nonce service")
ns.maxUsed = 2
n0 := ns.Nonce()
n1 := ns.Nonce()
n2 := ns.Nonce()
n3 := ns.Nonce()
n0, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
n1, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
n2, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
n3, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, ns.Valid(n3), "Rejected a valid nonce")
test.Assert(t, ns.Valid(n2), "Rejected a valid nonce")

View File

@ -231,17 +231,36 @@ func (log *AuditLogger) Audit(msg string) (err error) {
return log.auditAtLevel("Logging.Notice", msg)
}
func (log *AuditLogger) formatObjectMessage(msg string, obj interface{}) (string, error) {
jsonObj, err := json.Marshal(obj)
if err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
log.auditAtLevel("Logging.Err", fmt.Sprintf("Object could not be serialized to JSON. Raw: %+v", obj))
return "", err
}
return fmt.Sprintf("%s JSON=%s", msg, jsonObj), nil
}
// AuditObject sends a NOTICE-severity JSON-serialized object message that is prefixed
// with the audit tag, for special handling at the upstream system logger.
func (log *AuditLogger) AuditObject(msg string, obj interface{}) (err error) {
jsonLogEvent, logErr := json.Marshal(obj)
formattedEvent, logErr := log.formatObjectMessage(msg, obj)
if logErr != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
log.auditAtLevel("Logging.Err", fmt.Sprintf("%s - logEvent could not be serialized. Raw: %+v", msg, obj))
return logErr
}
return log.auditAtLevel("Logging.Notice", fmt.Sprintf("%s - %s", msg, jsonLogEvent))
return log.auditAtLevel("Logging.Notice", formattedEvent)
}
// InfoObject sends a INFO-severity JSON-serialized object message.
func (log *AuditLogger) InfoObject(msg string, obj interface{}) (err error) {
formattedEvent, logErr := log.formatObjectMessage(msg, obj)
if logErr != nil {
return logErr
}
return log.logAtLevel("Logging.Info", formattedEvent)
}
// AuditErr can format an error for auditing; it does so at ERR level.

View File

@ -596,37 +596,6 @@ var CAcertPEM = "-----BEGIN CERTIFICATE-----\n" +
// * Random public key
// * CN = not-example.com
// * DNSNames = not-example.com, www.not-example.com
/*
var CSRhex = "3082028c30820174020100301a311830160603550403130f" +
"6e6f742d6578616d706c652e636f6d30820122300d06092a" +
"864886f70d01010105000382010f003082010a0282010100" +
"aac67dd1e11fae980048b0ac91be005f21d9df8bb38461cc" +
"a7dfad601a00e91ae488c240a03ec53a5752a33d837d2d9c" +
"357c6a99ea7e55fe75482524480bb367aa85f75541bd0284" +
"ede1ab9b54925a5c9f88d08f9dc857ee707a59d3503b31ea" +
"64e42099acd70d2204c872ef49983e44cc2bc24389159fc5" +
"f6ca41b80540fb7a2fbf8aa43af7f539782f20f185d416cc" +
"66a88e5f8913a292b4a217e5b12e8244a9686af3b49ac88b" +
"215c6eb097c4befa3e66257a1358791e2bc471c18ba2ca6e" +
"161d2dcb53ebcb06e6b4b2e6cd42ff970581bc4971009cbd" +
"7ccc3f89648db720e2908a1be613a9c3afb46b477261c1bc" +
"c057bc749a102e6bd9dc45d87b2d97c50203010001a02d30" +
"2b06092a864886f70d01090e311e301c301a0603551d1104" +
"133011820f6e6f742d6578616d706c652e636f6d300d0609" +
"2a864886f70d01010b05000382010100a37025c2cb88b4fa" +
"f3c8417820a78a069b92cef45a3d2f3fbd18f41a07128258" +
"38bc55e6c1c416e5f34c365924391294741c23657ffa77e4" +
"aa3d589b560cbb9156aae175637cbb5061d69eefa7f432ca" +
"c9e4d03feaa367954cf6986a72ca76307c01852db72b43ae" +
"5ab7f673b81b58e06d8af1f681cd7c88d2fbc8c80d592a3f" +
"7c3ea20035b73c8e6afc8daea4d168fe469f7da9e4bac660" +
"fc207d1d93dce118dc7381e69fa4af37bb4d4d6d5342fa55" +
"7798a363aa04cf350ad1748e96eabee04fa379dd98524ea1" +
"53f07e1654e6077f4aaaf5c5b27edaf0385b48e0fc281424" +
"6363a01370c89e666169276eb133731cff2379d46d2fff9d" +
"f277b6d23fa24f9b"
*/
var CSRhex = "308202ae308201960201003027310b300906035504061302" +
"5553311830160603550403130f6e6f742d6578616d706c65" +
"2e636f6d30820122300d06092a864886f70d010101050003" +

View File

@ -8,6 +8,7 @@ package sa
import (
"database/sql"
"fmt"
"strings"
// Load both drivers to allow configuring either
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/go-sql-driver/mysql"
@ -33,6 +34,11 @@ var dialectMap = map[string]interface{}{
func NewDbMap(driver string, name string) (*gorp.DbMap, error) {
logger := blog.GetAuditLogger()
// We require this parameter for MySQL, so fail now if it is not present
if driver == "mysql" && !strings.Contains(name, "parseTime=true") {
return nil, fmt.Errorf("Database name must have parseTime=true")
}
db, err := sql.Open(driver, name)
if err != nil {
return nil, err

View File

@ -30,3 +30,8 @@ func TestForgottenDialect(t *testing.T) {
_, err := NewDbMap("sqlite3", ":memory:")
test.AssertError(t, err, "Shouldn't have found the dialect")
}
func TestParseTimeRequired(t *testing.T) {
_, err := NewDbMap("mysql", "invalid")
test.AssertError(t, err, "DB name must have parseTime=true")
}

View File

@ -7,7 +7,7 @@
"amqp": {
"server": "amqp://guest:guest@localhost:5672",
"-uncomment_for_AMQPS-ssl": {
"-uncomment_for_AMQPS-tls": {
"cacertfile": "/etc/boulder/rabbitmq-cacert.pem",
"certfile": "/etc/boulder/rabbitmq-cert.pem",
"keyfile": "/etc/boulder/rabbitmq-key.pem"
@ -105,7 +105,8 @@
"va": {
"dnsResolver": "8.8.8.8:53",
"dnsTimeout": "10s"
"dnsTimeout": "10s",
"userAgent": "boulder"
},
"sql": {

View File

@ -100,7 +100,8 @@
"va": {
"dnsResolver": "8.8.8.8:53",
"dnsTimeout": "10s"
"dnsTimeout": "10s",
"userAgent": "boulder"
},
"sql": {

View File

@ -244,7 +244,7 @@ module.exports = {
extensions: [{name: 'subjectAltName', altNames: sans}]
}]);
csr.sign(privateKey);
csr.sign(privateKey, forge.md.sha256.create());
// Convert CSR -> DER -> Base64
var der = forge.asn1.toDer(forge.pki.certificationRequestToAsn1(csr));

View File

@ -33,6 +33,7 @@ type ValidationAuthorityImpl struct {
Stats statsd.Statter
IssuerDomain string
TestMode bool
UserAgent string
}
// NewValidationAuthorityImpl constructs a new VA, and may place it
@ -64,7 +65,7 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
Type: core.MalformedProblem,
Detail: "No path provided for SimpleHTTP challenge.",
}
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] path empty: %s", identifier, challenge))
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] path empty: %v", identifier, challenge))
return challenge, challenge.Error
}
@ -80,26 +81,6 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
}
hostName := identifier.Value
// Check for DNSSEC failures for A/AAAA records
_, rtt, err := va.DNSResolver.LookupHost(hostName)
va.Stats.TimingDuration("DnsRtt.LookupHost", rtt, 1.0)
if err != nil {
if dnssecErr, ok := err.(core.DNSSECError); ok {
challenge.Error = &core.ProblemDetails{
Type: core.DNSSECProblem,
Detail: dnssecErr.Error(),
}
} else {
challenge.Error = &core.ProblemDetails{
Type: core.ServerInternalProblem,
Detail: "Unable to communicate with DNS server",
}
}
challenge.Status = core.StatusInvalid
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] DNS failure: %s", identifier, err))
return challenge, challenge.Error
}
var scheme string
if input.TLS == nil || (input.TLS != nil && *input.TLS) {
scheme = "https"
@ -126,6 +107,10 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
return challenge, err
}
if va.UserAgent != "" {
httpRequest.Header["User-Agent"] = []string{va.UserAgent}
}
httpRequest.Host = hostName
tr := &http.Transport{
// We are talking to a client that does not yet have a certificate,
@ -225,26 +210,6 @@ func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier,
z := sha256.Sum256(RS)
zName := fmt.Sprintf("%064x.acme.invalid", z)
// Check for DNSSEC failures for A/AAAA records
_, rtt, err := va.DNSResolver.LookupHost(identifier.Value)
va.Stats.TimingDuration("DnsRtt.LookupHost", rtt, 1.0)
if err != nil {
if dnssecErr, ok := err.(core.DNSSECError); ok {
challenge.Error = &core.ProblemDetails{
Type: core.DNSSECProblem,
Detail: dnssecErr.Error(),
}
} else {
challenge.Error = &core.ProblemDetails{
Type: core.ServerInternalProblem,
Detail: "Unable to communicate with DNS server",
}
}
challenge.Status = core.StatusInvalid
va.log.Debug(fmt.Sprintf("DVSNI [%s] DNS failure: %s", identifier, err))
return challenge, challenge.Error
}
// Make a connection with SNI = nonceName
hostPort := identifier.Value + ":443"
if va.TestMode {

View File

@ -20,6 +20,7 @@ import (
"runtime"
"strconv"
"strings"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
@ -89,15 +90,35 @@ func statusCodeFromError(err interface{}) int {
}
}
type requestEvent struct {
ID string `json:",omitempty"`
RealIP string `json:",omitempty"`
ForwardedFor string `json:",omitempty"`
Endpoint string `json:",omitempty"`
Method string `json:",omitempty"`
RequestTime time.Time `json:",omitempty"`
ResponseTime time.Time `json:",omitempty"`
Error string `json:",omitempty"`
Requester int64 `json:",omitempty"`
Contacts []core.AcmeURL `json:",omitempty"`
Extra map[string]interface{} `json:",omitempty"`
}
// NewWebFrontEndImpl constructs a web service for Boulder
func NewWebFrontEndImpl() WebFrontEndImpl {
func NewWebFrontEndImpl() (WebFrontEndImpl, error) {
logger := blog.GetAuditLogger()
logger.Notice("Web Front End Starting")
nonceService, err := core.NewNonceService()
if err != nil {
return WebFrontEndImpl{}, err
}
return WebFrontEndImpl{
log: logger,
nonceService: core.NewNonceService(),
}
nonceService: nonceService,
}, nil
}
// HandlePaths configures the HTTP engine to use various functions
@ -127,19 +148,24 @@ func (wfe *WebFrontEndImpl) HandlePaths() {
// Index serves a simple identification page. It is not part of the ACME spec.
func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
// http://golang.org/pkg/net/http/#example_ServeMux_Handle
// The "/" pattern matches everything, so we need to check
// that we're at the root here.
if request.URL.Path != "/" {
logEvent.Error = "Resource not found"
http.NotFound(response, request)
return
}
if request.Method != "GET" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
@ -166,7 +192,13 @@ func sendAllow(response http.ResponseWriter, methods ...string) {
}
func (wfe *WebFrontEndImpl) sendStandardHeaders(response http.ResponseWriter) {
response.Header().Set("Replay-Nonce", wfe.nonceService.Nonce())
// We do not propagate errors here, because (1) they should be
// transient, and (2) they fail closed.
nonce, err := wfe.nonceService.Nonce()
if err == nil {
response.Header().Set("Replay-Nonce", nonce)
}
response.Header().Set("Access-Control-Allow-Origin", "*")
}
@ -293,42 +325,53 @@ func link(url, relation string) string {
// NewRegistration is used by clients to submit a new registration/account
func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "POST")
wfe.sendError(response, "Method not allowed", "", http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, "", http.StatusMethodNotAllowed)
return
}
body, key, _, err := wfe.verifyPOST(request, false)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest)
return
}
if _, err = wfe.SA.GetRegistrationByKey(*key); err == nil {
wfe.sendError(response, "Registration key is already in use", nil, http.StatusConflict)
logEvent.Error = "Registration key is already in use"
wfe.sendError(response, logEvent.Error, nil, http.StatusConflict)
return
}
var init core.Registration
err = json.Unmarshal(body, &init)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error unmarshaling JSON", err, http.StatusBadRequest)
return
}
if len(init.Agreement) > 0 && init.Agreement != wfe.SubscriberAgreementURL {
wfe.sendError(response, fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", init.Agreement, wfe.SubscriberAgreementURL), nil, http.StatusBadRequest)
logEvent.Error = fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", init.Agreement, wfe.SubscriberAgreementURL)
wfe.sendError(response, logEvent.Error, nil, http.StatusBadRequest)
return
}
init.Key = *key
reg, err := wfe.RA.NewRegistration(init)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error creating new registration", err, statusCodeFromError(err))
return
}
logEvent.Requester = reg.ID
logEvent.Contacts = reg.Contact
// Use an explicitly typed variable. Otherwise `go vet' incorrectly complains
// that reg.ID is a string being passed to %d.
@ -336,6 +379,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques
regURL := fmt.Sprintf("%s%d", wfe.RegBase, id)
responseBody, err := json.Marshal(reg)
if err != nil {
logEvent.Error = err.Error()
// StatusInternalServerError because we just created this registration, it should be OK.
wfe.sendError(response, "Error marshaling registration", err, http.StatusInternalServerError)
return
@ -354,16 +398,21 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques
// NewAuthorization is used by clients to submit a new ID Authorization
func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
body, _, currReg, err := wfe.verifyPOST(request, true)
if err != nil {
logEvent.Error = err.Error()
if err == sql.ErrNoRows {
wfe.sendError(response, "No registration exists matching provided key", err, http.StatusForbidden)
} else {
@ -371,26 +420,33 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque
}
return
}
logEvent.Requester = currReg.ID
logEvent.Contacts = currReg.Contact
// Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update.
if currReg.Agreement == "" {
wfe.sendError(response, "Must agree to subscriber agreement before any further actions", nil, http.StatusForbidden)
logEvent.Error = "Must agree to subscriber agreement before any further actions"
wfe.sendError(response, logEvent.Error, nil, http.StatusForbidden)
return
}
var init core.Authorization
if err = json.Unmarshal(body, &init); err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error unmarshaling JSON", err, http.StatusBadRequest)
return
}
logEvent.Extra["Identifier"] = init.Identifier
// Create new authz and return
authz, err := wfe.RA.NewAuthorization(init, currReg.ID)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error creating new authz", err, statusCodeFromError(err))
return
}
logEvent.Extra["AuthzID"] = authz.ID
// Make a URL for this authz, then blow away the ID and RegID before serializing
authzURL := wfe.AuthzBase + string(authz.ID)
@ -398,6 +454,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque
authz.RegistrationID = 0
responseBody, err := json.Marshal(authz)
if err != nil {
logEvent.Error = err.Error()
// StatusInternalServerError because we generated the authz, it should be OK
wfe.sendError(response, "Error marshaling authz", err, http.StatusInternalServerError)
return
@ -408,17 +465,22 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque
response.Header().Set("Content-Type", "application/json")
response.WriteHeader(http.StatusCreated)
if _, err = response.Write(responseBody); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
}
// RevokeCertificate is used by clients to request the revocation of a cert.
func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
@ -426,27 +488,33 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
// because anyone with the right private key can revoke a certificate.
body, requestKey, registration, err := wfe.verifyPOST(request, false)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest)
return
}
logEvent.Requester = registration.ID
logEvent.Contacts = registration.Contact
type RevokeRequest struct {
CertificateDER core.JSONBuffer `json:"certificate"`
}
var revokeRequest RevokeRequest
if err = json.Unmarshal(body, &revokeRequest); err != nil {
logEvent.Error = err.Error()
wfe.log.Debug(fmt.Sprintf("Couldn't unmarshal in revoke request %s", string(body)))
wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest)
return
}
providedCert, err := x509.ParseCertificate(revokeRequest.CertificateDER)
if err != nil {
logEvent.Error = err.Error()
wfe.log.Debug("Couldn't parse cert in revoke request.")
wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest)
return
}
serial := core.SerialToString(providedCert.SerialNumber)
logEvent.Extra["ProvidedCertificateSerial"] = serial
cert, err := wfe.SA.GetCertificate(serial)
if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) {
wfe.sendError(response, "No such certificate", err, http.StatusNotFound)
@ -454,28 +522,37 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
}
parsedCertificate, err := x509.ParseCertificate(cert.DER)
if err != nil {
logEvent.Error = err.Error()
// InternalServerError because this is a failure to decode from our DB.
wfe.sendError(response, "Invalid certificate", err, http.StatusInternalServerError)
return
}
logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber)
logEvent.Extra["RetrievedCertificateDNSNames"] = parsedCertificate.DNSNames
logEvent.Extra["RetrievedCertificateEmailAddresses"] = parsedCertificate.EmailAddresses
logEvent.Extra["RetrievedCertificateIPAddresses"] = parsedCertificate.IPAddresses
certStatus, err := wfe.SA.GetCertificateStatus(serial)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Certificate status not yet available", err, http.StatusNotFound)
return
}
logEvent.Extra["CertificateStatus"] = certStatus.Status
if certStatus.Status == core.OCSPStatusRevoked {
wfe.sendError(response, "Certificate already revoked", "", http.StatusConflict)
logEvent.Error = "Certificate already revoked"
wfe.sendError(response, logEvent.Error, "", http.StatusConflict)
return
}
// TODO: Implement method of revocation by authorizations on account.
if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) ||
registration.ID == cert.RegistrationID) {
logEvent.Error = "Revocation request must be signed by private key of cert to be revoked"
wfe.log.Debug("Key mismatch for revoke")
wfe.sendError(response,
"Revocation request must be signed by private key of cert to be revoked",
logEvent.Error,
requestKey,
http.StatusForbidden)
return
@ -483,6 +560,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
err = wfe.RA.RevokeCertificate(*parsedCertificate)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Failed to revoke certificate", err, statusCodeFromError(err))
} else {
wfe.log.Debug(fmt.Sprintf("Revoked %v", serial))
@ -493,16 +571,21 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
// NewCertificate is used by clients to request the issuance of a cert for an
// authorized identifier.
func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
body, key, reg, err := wfe.verifyPOST(request, true)
if err != nil {
logEvent.Error = err.Error()
if err == sql.ErrNoRows {
wfe.sendError(response, "No registration exists matching provided key", err, http.StatusForbidden)
} else {
@ -510,20 +593,27 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
}
return
}
logEvent.Requester = reg.ID
logEvent.Contacts = reg.Contact
// Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update.
if reg.Agreement == "" {
wfe.sendError(response, "Must agree to subscriber agreement before any further actions", nil, http.StatusForbidden)
logEvent.Error = "Must agree to subscriber agreement before any further actions"
wfe.sendError(response, logEvent.Error, nil, http.StatusForbidden)
return
}
var init core.CertificateRequest
if err = json.Unmarshal(body, &init); err != nil {
fmt.Println(err)
logEvent.Error = err.Error()
wfe.sendError(response, "Error unmarshaling certificate request", err, http.StatusBadRequest)
return
}
logEvent.Extra["Authorizations"] = init.Authorizations
logEvent.Extra["CSRDNSNames"] = init.CSR.DNSNames
logEvent.Extra["CSREmailAddresses"] = init.CSR.EmailAddresses
logEvent.Extra["CSRIPAddresses"] = init.CSR.IPAddresses
wfe.log.Notice(fmt.Sprintf("Client requested new certificate: %v %v %v",
request.RemoteAddr, init, key))
@ -536,6 +626,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
// RA for secondary validation.
cert, err := wfe.RA.NewCertificate(init, reg.ID)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error creating new cert", err, statusCodeFromError(err))
return
}
@ -546,6 +637,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
// enumerate and mirror our certificates.
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response,
"Error creating new cert", err,
http.StatusBadRequest)
@ -560,17 +652,19 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
response.Header().Set("Content-Type", "application/pkix-cert")
response.WriteHeader(http.StatusCreated)
if _, err = response.Write(cert.DER); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
}
func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request) {
func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request, logEvent requestEvent) requestEvent {
wfe.sendStandardHeaders(response)
if request.Method != "GET" && request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET", "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
return
return logEvent
}
// Check that the requested challenge exists within the authorization
@ -586,24 +680,27 @@ func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.Re
}
if !found {
wfe.sendError(response, "Unable to find challenge", request.URL.RawQuery, http.StatusNotFound)
return
logEvent.Error = "Unable to find challenge"
wfe.sendError(response, logEvent.Error, request.URL.RawQuery, http.StatusNotFound)
return logEvent
}
switch request.Method {
default:
logEvent.Error = "Method not allowed"
sendAllow(response, "GET", "POST")
wfe.sendError(response, "Method not allowed", "", http.StatusMethodNotAllowed)
return
wfe.sendError(response, logEvent.Error, "", http.StatusMethodNotAllowed)
return logEvent
case "GET":
challenge := authz.Challenges[challengeIndex]
jsonReply, err := json.Marshal(challenge)
if err != nil {
logEvent.Error = err.Error()
// InternalServerError because this is a failure to decode data passed in
// by the caller, which got it from the DB.
wfe.sendError(response, "Failed to marshal challenge", err, http.StatusInternalServerError)
return
return logEvent
}
authzURL := wfe.AuthzBase + string(authz.ID)
@ -614,55 +711,65 @@ func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.Re
response.WriteHeader(http.StatusAccepted)
if _, err := response.Write(jsonReply); err != nil {
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
logEvent.Error = err.Error()
return logEvent
}
case "POST":
body, _, currReg, err := wfe.verifyPOST(request, true)
if err != nil {
logEvent.Error = err.Error()
if err == sql.ErrNoRows {
wfe.sendError(response, "No registration exists matching provided key", err, http.StatusForbidden)
} else {
wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest)
}
return
return logEvent
}
logEvent.Requester = currReg.ID
logEvent.Contacts = currReg.Contact
// Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update.
if currReg.Agreement == "" {
wfe.sendError(response, "Must agree to subscriber agreement before any further actions", nil, http.StatusForbidden)
return
logEvent.Error = "Must agree to subscriber agreement before any further actions"
wfe.sendError(response, logEvent.Error, nil, http.StatusForbidden)
return logEvent
}
// Check that the registration ID matching the key used matches
// the registration ID on the authz object
if currReg.ID != authz.RegistrationID {
logEvent.Error = fmt.Sprintf("User: %v != Authorization: %v", currReg.ID, authz.RegistrationID)
wfe.sendError(response, "User registration ID doesn't match registration ID in authorization",
fmt.Sprintf("User: %v != Authorization: %v", currReg.ID, authz.RegistrationID),
logEvent.Error,
http.StatusForbidden)
return
return logEvent
}
var challengeResponse core.Challenge
if err = json.Unmarshal(body, &challengeResponse); err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error unmarshaling challenge response", err, http.StatusBadRequest)
return
return logEvent
}
// Ask the RA to update this authorization
updatedAuthz, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeResponse)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Unable to update authorization", err, statusCodeFromError(err))
return
return logEvent
}
challenge := updatedAuthz.Challenges[challengeIndex]
// assumption: UpdateAuthorization does not modify order of challenges
jsonReply, err := json.Marshal(challenge)
if err != nil {
logEvent.Error = err.Error()
// StatusInternalServerError because we made the challenges, they should be OK
wfe.sendError(response, "Failed to marshal challenge", err, http.StatusInternalServerError)
return
return logEvent
}
authzURL := wfe.AuthzBase + string(authz.ID)
@ -672,24 +779,32 @@ func (wfe *WebFrontEndImpl) challenge(authz core.Authorization, response http.Re
response.Header().Add("Link", link(authzURL, "up"))
response.WriteHeader(http.StatusAccepted)
if _, err = response.Write(jsonReply); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
return logEvent
}
}
return logEvent
}
// Registration is used by a client to submit an update to their registration.
func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
body, _, currReg, err := wfe.verifyPOST(request, true)
if err != nil {
logEvent.Error = err.Error()
if err == sql.ErrNoRows {
wfe.sendError(response,
"No registration exists matching provided key",
@ -700,33 +815,38 @@ func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *
}
return
}
logEvent.Requester = currReg.ID
logEvent.Contacts = currReg.Contact
// Requests to this handler should have a path that leads to a known
// registration
idStr := parseIDFromPath(request.URL.Path)
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Registration ID must be an integer", err, http.StatusBadRequest)
return
} else if id <= 0 {
wfe.sendError(response, "Registration ID must be a positive non-zero integer", id, http.StatusBadRequest)
logEvent.Error = "Registration ID must be a positive non-zero integer"
wfe.sendError(response, logEvent.Error, id, http.StatusBadRequest)
return
} else if id != currReg.ID {
wfe.sendError(response, "Request signing key did not match registration key", "", http.StatusForbidden)
logEvent.Error = "Request signing key did not match registration key"
wfe.sendError(response, logEvent.Error, "", http.StatusForbidden)
return
}
var update core.Registration
err = json.Unmarshal(body, &update)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Error unmarshaling registration", err, http.StatusBadRequest)
return
}
if len(update.Agreement) > 0 && update.Agreement != wfe.SubscriberAgreementURL {
wfe.sendError(response,
fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]",
update.Agreement, wfe.SubscriberAgreementURL), nil, http.StatusBadRequest)
logEvent.Error = fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", update.Agreement, wfe.SubscriberAgreementURL)
wfe.sendError(response, logEvent.Error, nil, http.StatusBadRequest)
return
}
@ -739,12 +859,14 @@ func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *
// Ask the RA to update this authorization.
updatedReg, err := wfe.RA.UpdateRegistration(currReg, update)
if err != nil {
logEvent.Error = err.Error()
wfe.sendError(response, "Unable to update registration", err, statusCodeFromError(err))
return
}
jsonReply, err := json.Marshal(updatedReg)
if err != nil {
logEvent.Error = err.Error()
// StatusInternalServerError because we just generated the reg, it should be OK
wfe.sendError(response, "Failed to marshal registration", err, http.StatusInternalServerError)
return
@ -757,11 +879,15 @@ func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *
// Authorization is used by clients to submit an update to one of their
// authorizations.
func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "GET" && request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET", "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
@ -774,17 +900,23 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
http.StatusNotFound)
return
}
logEvent.Extra["AuthorizationID"] = authz.ID
logEvent.Extra["AuthorizationRegistrationID"] = authz.RegistrationID
logEvent.Extra["AuthorizationIdentifier"] = authz.Identifier
logEvent.Extra["AuthorizationStatus"] = authz.Status
logEvent.Extra["AuthorizationExpires"] = authz.Expires
// If there is a fragment, then this is actually a request to a challenge URI
if len(request.URL.RawQuery) != 0 {
wfe.challenge(authz, response, request)
logEvent = wfe.challenge(authz, response, request, logEvent)
return
}
switch request.Method {
default:
logEvent.Error = "Method not allowed"
sendAllow(response, "GET", "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
case "GET":
@ -794,6 +926,7 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
jsonReply, err := json.Marshal(authz)
if err != nil {
logEvent.Error = err.Error()
// InternalServerError because this is a failure to decode from our DB.
wfe.sendError(response, "Failed to marshal authz", err, http.StatusInternalServerError)
return
@ -802,6 +935,7 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
response.Header().Set("Content-Type", "application/json")
response.WriteHeader(http.StatusOK)
if _, err = response.Write(jsonReply); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
}
@ -812,37 +946,39 @@ var allHex = regexp.MustCompile("^[0-9a-f]+$")
// Certificate is used by clients to request a copy of their current certificate, or to
// request a reissuance of the certificate.
func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "GET" && request.Method != "POST" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET", "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
return
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
}
path := request.URL.Path
switch request.Method {
default:
sendAllow(response, "GET", "POST")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
return
case "GET":
// Certificate paths consist of the CertBase path, plus exactly sixteen hex
// digits.
if !strings.HasPrefix(path, CertPath) {
wfe.sendError(response, "Not found", path, http.StatusNotFound)
logEvent.Error = "Certificate not found"
wfe.sendError(response, logEvent.Error, path, http.StatusNotFound)
return
}
serial := path[len(CertPath):]
if len(serial) != 16 || !allHex.Match([]byte(serial)) {
wfe.sendError(response, "Not found", serial, http.StatusNotFound)
logEvent.Error = "Certificate not found"
wfe.sendError(response, logEvent.Error, serial, http.StatusNotFound)
return
}
wfe.log.Debug(fmt.Sprintf("Requested certificate ID %s", serial))
logEvent.Extra["RequestedSerial"] = serial
cert, err := wfe.SA.GetCertificateByShortSerial(serial)
if err != nil {
logEvent.Error = err.Error()
if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") {
wfe.sendError(response, "Multiple certificates with same short serial", err, http.StatusConflict)
} else {
@ -856,11 +992,13 @@ func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *h
response.Header().Add("Link", link(IssuerPath, "up"))
response.WriteHeader(http.StatusOK)
if _, err = response.Write(cert.DER); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
return
case "POST":
wfe.sendError(response, "Not yet supported", "", http.StatusNotFound)
logEvent.Error = "Not yet supported"
wfe.sendError(response, logEvent.Error, "", http.StatusNotFound)
return
}
}
@ -868,11 +1006,15 @@ func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *h
// Terms is used by the client to obtain the current Terms of Service /
// Subscriber Agreement to which the subscriber must agree.
func (wfe *WebFrontEndImpl) Terms(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "GET" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
@ -881,9 +1023,13 @@ func (wfe *WebFrontEndImpl) Terms(response http.ResponseWriter, request *http.Re
// Issuer obtains the issuer certificate used by this instance of Boulder.
func (wfe *WebFrontEndImpl) Issuer(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "GET" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
return
@ -893,15 +1039,20 @@ func (wfe *WebFrontEndImpl) Issuer(response http.ResponseWriter, request *http.R
response.Header().Set("Content-Type", "application/pkix-cert")
response.WriteHeader(http.StatusOK)
if _, err := response.Write(wfe.IssuerCert); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
}
// BuildID tells the requestor what build we're running.
func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http.Request) {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
wfe.sendStandardHeaders(response)
if request.Method != "GET" {
logEvent.Error = "Method not allowed"
sendAllow(response, "GET")
wfe.sendError(response, "Method not allowed", request.Method, http.StatusMethodNotAllowed)
return
@ -911,6 +1062,33 @@ func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http.
response.WriteHeader(http.StatusOK)
detailsString := fmt.Sprintf("Boulder=(%s %s) Golang=(%s) BuildHost=(%s)", core.GetBuildID(), core.GetBuildTime(), runtime.Version(), core.GetBuildHost())
if _, err := fmt.Fprintln(response, detailsString); err != nil {
logEvent.Error = err.Error()
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
}
func (wfe *WebFrontEndImpl) logRequestDetails(logEvent *requestEvent) {
logEvent.ResponseTime = time.Now()
var msg string
if logEvent.Error != "" {
msg = "Terminated request"
} else {
msg = "Successful request"
}
wfe.log.InfoObject(msg, logEvent)
}
func (wfe *WebFrontEndImpl) populateRequestEvent(request *http.Request) (logEvent requestEvent) {
logEvent = requestEvent{
ID: core.NewToken(),
RealIP: request.Header.Get("X-Real-IP"),
ForwardedFor: request.Header.Get("X-Forwarded-For"),
Method: request.Method,
RequestTime: time.Now(),
Extra: make(map[string]interface{}, 0),
}
if request.URL != nil {
logEvent.Endpoint = request.URL.String()
}
return
}

View File

@ -308,14 +308,17 @@ func signRequest(t *testing.T, req string, nonceService *core.NonceService) stri
test.AssertNotError(t, err, "Failed to unmarshal key")
signer, err := jose.NewSigner("RS256", &accountKey)
test.AssertNotError(t, err, "Failed to make signer")
result, err := signer.Sign([]byte(req), nonceService.Nonce())
nonce, err := nonceService.Nonce()
test.AssertNotError(t, err, "Failed to make nonce")
result, err := signer.Sign([]byte(req), nonce)
test.AssertNotError(t, err, "Failed to sign req")
ret := result.FullSerialize()
return ret
}
func setupWFE() WebFrontEndImpl {
wfe := NewWebFrontEndImpl()
func setupWFE(t *testing.T) WebFrontEndImpl {
wfe, err := NewWebFrontEndImpl()
test.AssertNotError(t, err, "Unable to create WFE")
wfe.NewReg = wfe.BaseURL + NewRegPath
wfe.RegBase = wfe.BaseURL + RegPath
@ -331,7 +334,7 @@ func setupWFE() WebFrontEndImpl {
}
func TestStandardHeaders(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
cases := []struct {
path string
@ -366,7 +369,7 @@ func TestStandardHeaders(t *testing.T) {
}
func TestIndex(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
responseWriter := httptest.NewRecorder()
@ -392,7 +395,7 @@ func TestIndex(t *testing.T) {
// TODO: Write additional test cases for:
// - RA returns with a failure
func TestIssueCertificate(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
// TODO: Use a mock RA so we can test various conditions of authorized, not authorized, etc.
ra := ra.NewRegistrationAuthorityImpl()
@ -521,7 +524,7 @@ func TestIssueCertificate(t *testing.T) {
}
func TestChallenge(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -557,7 +560,7 @@ func TestChallenge(t *testing.T) {
Method: "POST",
URL: challengeURL,
Body: makeBody(signRequest(t, "{}", &wfe.nonceService)),
})
}, requestEvent{})
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
@ -571,7 +574,7 @@ func TestChallenge(t *testing.T) {
}
func TestNewRegistration(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -608,7 +611,9 @@ func TestNewRegistration(t *testing.T) {
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset()
result, err := signer.Sign([]byte("foo"), wfe.nonceService.Nonce())
nonce, err := wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err := signer.Sign([]byte("foo"), nonce)
wfe.NewRegistration(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),
@ -642,9 +647,11 @@ func TestNewRegistration(t *testing.T) {
"{\"type\":\"urn:acme:error:malformed\",\"detail\":\"Unable to read/verify body\"}")
responseWriter.Body.Reset()
nonce, err = wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err = signer.Sign(
[]byte("{\"contact\":[\"tel:123456789\"],\"agreement\":\"https://letsencrypt.org/im-bad\"}"),
wfe.nonceService.Nonce())
nonce)
wfe.NewRegistration(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),
@ -654,8 +661,9 @@ func TestNewRegistration(t *testing.T) {
"{\"type\":\"urn:acme:error:malformed\",\"detail\":\"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL ["+agreementURL+"]\"}")
responseWriter.Body.Reset()
result, err = signer.Sign([]byte("{\"contact\":[\"tel:123456789\"],\"agreement\":\""+agreementURL+"\"}"),
wfe.nonceService.Nonce())
nonce, err = wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err = signer.Sign([]byte("{\"contact\":[\"tel:123456789\"],\"agreement\":\""+agreementURL+"\"}"), nonce)
wfe.NewRegistration(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),
@ -685,7 +693,9 @@ func TestNewRegistration(t *testing.T) {
// POST, Valid JSON, Key already in use
responseWriter.Body.Reset()
result, err = signer.Sign([]byte("{\"contact\":[\"tel:123456789\"],\"agreement\":\""+agreementURL+"\"}"), wfe.nonceService.Nonce())
nonce, err = wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err = signer.Sign([]byte("{\"contact\":[\"tel:123456789\"],\"agreement\":\""+agreementURL+"\"}"), nonce)
wfe.NewRegistration(responseWriter, &http.Request{
Method: "POST",
@ -719,14 +729,16 @@ func TestRevokeCertificate(t *testing.T) {
test.AssertNotError(t, err, "Failed to marshal request")
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
wfe := setupWFE()
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
wfe.SubscriberAgreementURL = agreementURL
responseWriter := httptest.NewRecorder()
responseWriter.Body.Reset()
result, _ := signer.Sign(revokeRequestJSON, wfe.nonceService.Nonce())
nonce, err := wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, _ := signer.Sign(revokeRequestJSON, nonce)
wfe.RevokeCertificate(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),
@ -743,7 +755,9 @@ func TestRevokeCertificate(t *testing.T) {
test.Assert(t, ok, "Couldn't load RSA key")
accountKeySigner, err := jose.NewSigner("RS256", test1Key)
test.AssertNotError(t, err, "Failed to make signer")
result, _ = accountKeySigner.Sign(revokeRequestJSON, wfe.nonceService.Nonce())
nonce, err = wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, _ = accountKeySigner.Sign(revokeRequestJSON, nonce)
wfe.RevokeCertificate(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),
@ -775,14 +789,16 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
test.AssertNotError(t, err, "Failed to marshal request")
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
wfe := setupWFE()
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
wfe.SubscriberAgreementURL = agreementURL
responseWriter := httptest.NewRecorder()
responseWriter.Body.Reset()
result, _ := signer.Sign(revokeRequestJSON, wfe.nonceService.Nonce())
nonce, err := wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, _ := signer.Sign(revokeRequestJSON, nonce)
wfe.RevokeCertificate(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),
@ -793,7 +809,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
}
func TestAuthorization(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -875,7 +891,7 @@ func TestAuthorization(t *testing.T) {
}
func TestRegistration(t *testing.T) {
wfe := setupWFE()
wfe := setupWFE(t)
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -925,7 +941,9 @@ func TestRegistration(t *testing.T) {
test.AssertNotError(t, err, "Failed to make signer")
// Test POST valid JSON but key is not registered
result, err := signer.Sign([]byte("{\"agreement\":\""+agreementURL+"\"}"), wfe.nonceService.Nonce())
nonce, err := wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err := signer.Sign([]byte("{\"agreement\":\""+agreementURL+"\"}"), nonce)
path, _ = url.Parse("/2")
wfe.Registration(responseWriter, &http.Request{
Method: "POST",
@ -947,7 +965,9 @@ func TestRegistration(t *testing.T) {
path, _ = url.Parse("/2")
// Test POST valid JSON with registration up in the mock (with incorrect agreement URL)
result, err = signer.Sign([]byte("{\"agreement\":\"https://letsencrypt.org/im-bad\"}"), wfe.nonceService.Nonce())
nonce, err = wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err = signer.Sign([]byte("{\"agreement\":\"https://letsencrypt.org/im-bad\"}"), nonce)
// Test POST valid JSON with registration up in the mock
path, _ = url.Parse("/1")
@ -962,7 +982,9 @@ func TestRegistration(t *testing.T) {
responseWriter.Body.Reset()
// Test POST valid JSON with registration up in the mock (with correct agreement URL)
result, err = signer.Sign([]byte("{\"agreement\":\""+agreementURL+"\"}"), wfe.nonceService.Nonce())
nonce, err = wfe.nonceService.Nonce()
test.AssertNotError(t, err, "Unable to create nonce")
result, err = signer.Sign([]byte("{\"agreement\":\""+agreementURL+"\"}"), nonce)
wfe.Registration(responseWriter, &http.Request{
Method: "POST",
Body: makeBody(result.FullSerialize()),