Merge branch 'master' into sig-reuse
This commit is contained in:
commit
275b086acc
13
Dockerfile
13
Dockerfile
|
|
@ -33,7 +33,7 @@ EXPOSE 4000
|
|||
ENV BOULDER_CONFIG /go/src/github.com/letsencrypt/boulder/test/boulder-config.json
|
||||
|
||||
# Get the Let's Encrypt client
|
||||
RUN git clone https://www.github.com/letsencrypt/lets-encrypt-preview.git /letsencrypt
|
||||
RUN git clone https://www.github.com/letsencrypt/letsencrypt.git /letsencrypt
|
||||
WORKDIR /letsencrypt
|
||||
RUN ./bootstrap/debian.sh && \
|
||||
apt-get clean && \
|
||||
|
|
@ -43,20 +43,13 @@ RUN ./bootstrap/debian.sh && \
|
|||
RUN virtualenv --no-site-packages -p python2 venv && \
|
||||
./venv/bin/pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx
|
||||
|
||||
RUN go get bitbucket.org/liamstask/goose/cmd/goose
|
||||
|
||||
ENV LETSENCRYPT_PATH /letsencrypt
|
||||
|
||||
# Copy in the Boulder sources
|
||||
COPY . /go/src/github.com/letsencrypt/boulder
|
||||
|
||||
# Build Boulder
|
||||
RUN go install \
|
||||
github.com/letsencrypt/boulder/cmd/activity-monitor \
|
||||
github.com/letsencrypt/boulder/cmd/boulder-ca \
|
||||
github.com/letsencrypt/boulder/cmd/boulder-ra \
|
||||
github.com/letsencrypt/boulder/cmd/boulder-sa \
|
||||
github.com/letsencrypt/boulder/cmd/boulder-va \
|
||||
github.com/letsencrypt/boulder/cmd/boulder-wfe
|
||||
|
||||
WORKDIR /go/src/github.com/letsencrypt/boulder
|
||||
CMD ["bash", "-c", "service mysql start && \
|
||||
service rsyslog start && \
|
||||
|
|
|
|||
62
README.md
62
README.md
|
|
@ -7,49 +7,30 @@ This is an initial implementation of an ACME-based CA. The [ACME protocol](https
|
|||
[](https://travis-ci.org/letsencrypt/boulder)
|
||||
[](https://coveralls.io/r/letsencrypt/boulder)
|
||||
|
||||
Docker
|
||||
Quickstart
|
||||
------
|
||||
|
||||
Boulder is available as a [Docker image from Quay.io](https://quay.io/repository/letsencrypt/boulder). The Docker image expects the `config.json` file to be located at `/boulder/config.json` within the container.
|
||||
Boulder has a Dockerfile to make it easy to install and set up all its
|
||||
dependencies. This approach is most suitable if you just need to set up Boulder
|
||||
for the purpose of testing client software against it. To start Boulder
|
||||
in a Docker container, run:
|
||||
|
||||
(Note: You can override the `config.json` location by specifying a different BOULDER_CONFIG environment variable, such as with `-e BOULDER_CONFIG=mypath/myfile.config`.)
|
||||
./test/run-docker.sh
|
||||
|
||||
There are no default commands; you must choose one of the executables from the `cmd` path.
|
||||
|
||||
There are several tags available:
|
||||
- `stable` is maintained by the Let's Encrypt team as a fairly stable copy of Boulder.
|
||||
- `latest` is a more recent build of Boulder. It may lag behind the `master` ref, as automated builds are being reworked.
|
||||
- Tags for individual short-format git refs, representing those builds.
|
||||
|
||||
|
||||
A quick-start method for running a Boulder instance is to use one of the example configurations:
|
||||
|
||||
docker run -i --name=boulder --read-only=true --rm=true -p 4000:4000 quay.io/letsencrypt/boulder:latest
|
||||
|
||||
|
||||
Alternatively, to run all services locally, using AMQP to pass messages between them, you can use:
|
||||
|
||||
```
|
||||
> python start.py
|
||||
# start.py will use the configuration specified by BOULDER_CONFIG or test/boulder-config.json
|
||||
```
|
||||
|
||||
To run a single module, specifying the AMQP server, you might use something more like:
|
||||
|
||||
```
|
||||
> docker run --name=boulder --read-only=true --rm=true -v $(pwd)/.boulder-config:/boulder:ro quay.io/letsencrypt/boulder:stable boulder-ra
|
||||
```
|
||||
|
||||
|
||||
|
||||
Quickstart
|
||||
Slow start
|
||||
----------
|
||||
|
||||
Boulder requires an installation of RabbitMQ, libtool-ltdl, and
|
||||
This approach is better if you intend to develop on Boulder frequently, because
|
||||
it's challenging to develop inside the Docker container.
|
||||
|
||||
Boulder requires an installation of RabbitMQ, libtool-ltdl, goose, and
|
||||
MariaDB 10 to work correctly. On Ubuntu and CentOS, you may have to
|
||||
install RabbitMQ from https://rabbitmq.com/download.html to get a
|
||||
recent version.
|
||||
|
||||
Also, Boulder requires Go 1.5. As of September 2015 this version is not yet
|
||||
available in OS repostiories, so you will have to install from https://golang.org/dl/.
|
||||
|
||||
Ubuntu:
|
||||
|
||||
sudo apt-get install libltdl3-dev mariadb-server rabbitmq-server
|
||||
|
|
@ -69,19 +50,22 @@ or
|
|||
(On OS X, using port, you will have to add `CGO_CFLAGS="-I/opt/local/include" CGO_LDFLAGS="-L/opt/local/lib"` to your environment or `go` invocations.)
|
||||
|
||||
```
|
||||
> go get bitbucket.org/liamstask/goose/cmd/goose
|
||||
> go get github.com/letsencrypt/boulder/ # Ignore errors about no buildable files
|
||||
> cd $GOPATH/src/github.com/letsencrypt/boulder
|
||||
> ./test/create_db.sh
|
||||
# This starts each Boulder component with test configs. Ctrl-C kills all.
|
||||
> ./start.py
|
||||
# Run tests
|
||||
> ./test.sh
|
||||
```
|
||||
|
||||
The databases that boulder requires to operate in development and
|
||||
testing can be created using test/create\_db.sh. It uses the root
|
||||
MariaDB user, so if you have disabled that account you may have to
|
||||
adjust the file or recreate the commands.
|
||||
Note: create\_db.sh it uses the root MariaDB user, so if you
|
||||
have disabled that account you may have to adjust the file or
|
||||
recreate the commands.
|
||||
|
||||
You can also check out the official client from
|
||||
https://github.com/letsencrypt/lets-encrypt-preview/ and follow the setup
|
||||
https://github.com/letsencrypt/letsencrypt/ and follow the setup
|
||||
instructions there.
|
||||
|
||||
Component Model
|
||||
|
|
@ -130,7 +114,7 @@ and to [avoid insecure fallback in go
|
|||
get](https://github.com/golang/go/issues/9637).
|
||||
|
||||
Local development also requires a RabbitMQ installation and MariaDB
|
||||
10 installation. MariaDB should be run on port 3306 for the
|
||||
10 installation (see above). MariaDB should be run on port 3306 for the
|
||||
default integration tests.
|
||||
|
||||
To update the Go dependencies:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -194,7 +195,7 @@ func (c *certChecker) checkCert(cert core.Certificate) (problems []string) {
|
|||
}
|
||||
}
|
||||
// Check the cert has the correct key usage extensions
|
||||
if !core.CmpExtKeyUsageSlice(parsedCert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
|
||||
if !reflect.DeepEqual(parsedCert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
|
||||
problems = append(problems, "Certificate has incorrect key usage extensions")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
core/dns.go
20
core/dns.go
|
|
@ -45,9 +45,9 @@ var (
|
|||
|
||||
// DNSResolverImpl represents a client that talks to an external resolver
|
||||
type DNSResolverImpl struct {
|
||||
DNSClient *dns.Client
|
||||
Servers []string
|
||||
allowLoopbackAddresses bool
|
||||
DNSClient *dns.Client
|
||||
Servers []string
|
||||
allowRestrictedAddresses bool
|
||||
}
|
||||
|
||||
// NewDNSResolverImpl constructs a new DNS resolver object that utilizes the
|
||||
|
|
@ -59,9 +59,9 @@ func NewDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolve
|
|||
dnsClient.DialTimeout = dialTimeout
|
||||
|
||||
return &DNSResolverImpl{
|
||||
DNSClient: dnsClient,
|
||||
Servers: servers,
|
||||
allowLoopbackAddresses: false,
|
||||
DNSClient: dnsClient,
|
||||
Servers: servers,
|
||||
allowRestrictedAddresses: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ func NewDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolve
|
|||
// This constructor should *only* be called from tests (unit or integration).
|
||||
func NewTestDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolverImpl {
|
||||
resolver := NewDNSResolverImpl(dialTimeout, servers)
|
||||
resolver.allowLoopbackAddresses = true
|
||||
resolver.allowRestrictedAddresses = true
|
||||
return resolver
|
||||
}
|
||||
|
||||
|
|
@ -120,8 +120,8 @@ func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.D
|
|||
return txt, rtt, err
|
||||
}
|
||||
|
||||
func isPrivateV4(ip net.IP, allowLoopback bool) bool {
|
||||
return rfc1918_10.Contains(ip) || rfc1918_172_16.Contains(ip) || rfc1918_192_168.Contains(ip) || (!allowLoopback && rfc5735_127.Contains(ip))
|
||||
func isPrivateV4(ip net.IP) bool {
|
||||
return rfc1918_10.Contains(ip) || rfc1918_172_16.Contains(ip) || rfc1918_192_168.Contains(ip) || rfc5735_127.Contains(ip)
|
||||
}
|
||||
|
||||
// LookupHost sends a DNS query to find all A records associated with the provided
|
||||
|
|
@ -141,7 +141,7 @@ func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.
|
|||
|
||||
for _, answer := range r.Answer {
|
||||
if answer.Header().Rrtype == dns.TypeA {
|
||||
if a, ok := answer.(*dns.A); ok && a.A.To4() != nil && !isPrivateV4(a.A, dnsResolver.allowLoopbackAddresses) {
|
||||
if a, ok := answer.(*dns.A); ok && a.A.To4() != nil && (!isPrivateV4(a.A) || dnsResolver.allowRestrictedAddresses) {
|
||||
addrs = append(addrs, a.A)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
123
core/objects.go
123
core/objects.go
|
|
@ -11,7 +11,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -101,52 +100,6 @@ func (pd *ProblemDetails) Error() string {
|
|||
return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail)
|
||||
}
|
||||
|
||||
func cmpStrSlice(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
sort.Strings(a)
|
||||
sort.Strings(b)
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func CmpExtKeyUsageSlice(a, b []x509.ExtKeyUsage) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
testMap := make(map[int]bool, len(a))
|
||||
for i := range a {
|
||||
testMap[int(a[i])] = true
|
||||
}
|
||||
for i := range b {
|
||||
if !testMap[int(b[i])] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cmpIPSlice(a, b []net.IP) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
testMap := make(map[string]bool, len(a))
|
||||
for i := range a {
|
||||
testMap[a[i].String()] = true
|
||||
}
|
||||
for i := range b {
|
||||
if !testMap[b[i].String()] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// An AcmeIdentifier encodes an identifier that can
|
||||
// be validated by ACME. The protocol allows for different
|
||||
// types of identifier to be supported (DNS names, IP
|
||||
|
|
@ -541,82 +494,6 @@ type ExternalCert struct {
|
|||
CertDER []byte `db:"rawDERCert"` // DER (binary) encoding of the raw certificate
|
||||
}
|
||||
|
||||
// MatchesCSR tests the contents of a generated certificate to make sure
|
||||
// that the PublicKey, CommonName, and DNSNames match those provided in
|
||||
// the CSR that was used to generate the certificate. It also checks the
|
||||
// following fields for:
|
||||
// * notAfter is after earliestExpiry
|
||||
// * notBefore is not more than 24 hours ago
|
||||
// * BasicConstraintsValid is true
|
||||
// * IsCA is false
|
||||
// * ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth
|
||||
// * Subject only contains CommonName & Names
|
||||
func (cert Certificate) MatchesCSR(csr *x509.CertificateRequest, earliestExpiry time.Time) (err error) {
|
||||
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check issued certificate matches what was expected from the CSR
|
||||
hostNames := make([]string, len(csr.DNSNames))
|
||||
copy(hostNames, csr.DNSNames)
|
||||
if len(csr.Subject.CommonName) > 0 {
|
||||
hostNames = append(hostNames, csr.Subject.CommonName)
|
||||
}
|
||||
hostNames = UniqueNames(hostNames)
|
||||
|
||||
if !KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) {
|
||||
err = InternalServerError("Generated certificate public key doesn't match CSR public key")
|
||||
return
|
||||
}
|
||||
if len(csr.Subject.CommonName) > 0 && parsedCertificate.Subject.CommonName != csr.Subject.CommonName {
|
||||
err = InternalServerError("Generated certificate CommonName doesn't match CSR CommonName")
|
||||
return
|
||||
}
|
||||
if !cmpStrSlice(parsedCertificate.DNSNames, hostNames) {
|
||||
err = InternalServerError("Generated certificate DNSNames don't match CSR DNSNames")
|
||||
return
|
||||
}
|
||||
if !cmpIPSlice(parsedCertificate.IPAddresses, csr.IPAddresses) {
|
||||
err = InternalServerError("Generated certificate IPAddresses don't match CSR IPAddresses")
|
||||
return
|
||||
}
|
||||
if !cmpStrSlice(parsedCertificate.EmailAddresses, csr.EmailAddresses) {
|
||||
err = InternalServerError("Generated certificate EmailAddresses don't match CSR EmailAddresses")
|
||||
return
|
||||
}
|
||||
if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 ||
|
||||
len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 ||
|
||||
len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 ||
|
||||
len(parsedCertificate.Subject.PostalCode) > 0 || len(parsedCertificate.Subject.SerialNumber) > 0 {
|
||||
err = InternalServerError("Generated certificate Subject contains fields other than CommonName or Names")
|
||||
return
|
||||
}
|
||||
if parsedCertificate.NotAfter.After(earliestExpiry) {
|
||||
err = InternalServerError("Generated certificate expires before earliest expiration")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 {
|
||||
err = InternalServerError(fmt.Sprintf("Generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore)))
|
||||
return
|
||||
}
|
||||
if !parsedCertificate.BasicConstraintsValid {
|
||||
err = InternalServerError("Generated certificate doesn't have basic constraints set")
|
||||
return
|
||||
}
|
||||
if parsedCertificate.IsCA {
|
||||
err = InternalServerError("Generated certificate can sign other certificates")
|
||||
return
|
||||
}
|
||||
if !CmpExtKeyUsageSlice(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
|
||||
err = InternalServerError("Generated certificate doesn't have correct key usage extensions")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CertificateStatus structs are internal to the server. They represent the
|
||||
// latest data about the status of the certificate, required for OCSP updating
|
||||
// and for validating that the subscriber has accepted the certificate.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -203,6 +205,89 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
|
|||
return authz, err
|
||||
}
|
||||
|
||||
// MatchesCSR tests the contents of a generated certificate to make sure
|
||||
// that the PublicKey, CommonName, and DNSNames match those provided in
|
||||
// the CSR that was used to generate the certificate. It also checks the
|
||||
// following fields for:
|
||||
// * notAfter is after earliestExpiry
|
||||
// * notBefore is not more than 24 hours ago
|
||||
// * BasicConstraintsValid is true
|
||||
// * IsCA is false
|
||||
// * ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth
|
||||
// * Subject only contains CommonName & Names
|
||||
func (ra *RegistrationAuthorityImpl) MatchesCSR(
|
||||
cert core.Certificate,
|
||||
csr *x509.CertificateRequest,
|
||||
earliestExpiry time.Time) (err error) {
|
||||
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check issued certificate matches what was expected from the CSR
|
||||
hostNames := make([]string, len(csr.DNSNames))
|
||||
copy(hostNames, csr.DNSNames)
|
||||
if len(csr.Subject.CommonName) > 0 {
|
||||
hostNames = append(hostNames, csr.Subject.CommonName)
|
||||
}
|
||||
hostNames = core.UniqueNames(hostNames)
|
||||
|
||||
if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) {
|
||||
err = core.InternalServerError("Generated certificate public key doesn't match CSR public key")
|
||||
return
|
||||
}
|
||||
if len(csr.Subject.CommonName) > 0 && parsedCertificate.Subject.CommonName != csr.Subject.CommonName {
|
||||
err = core.InternalServerError("Generated certificate CommonName doesn't match CSR CommonName")
|
||||
return
|
||||
}
|
||||
// Sort both slices of names before comparison.
|
||||
parsedNames := parsedCertificate.DNSNames
|
||||
sort.Strings(parsedNames)
|
||||
sort.Strings(hostNames)
|
||||
if !reflect.DeepEqual(parsedNames, hostNames) {
|
||||
err = core.InternalServerError("Generated certificate DNSNames don't match CSR DNSNames")
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(parsedCertificate.IPAddresses, csr.IPAddresses) {
|
||||
err = core.InternalServerError("Generated certificate IPAddresses don't match CSR IPAddresses")
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(parsedCertificate.EmailAddresses, csr.EmailAddresses) {
|
||||
err = core.InternalServerError("Generated certificate EmailAddresses don't match CSR EmailAddresses")
|
||||
return
|
||||
}
|
||||
if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 ||
|
||||
len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 ||
|
||||
len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 ||
|
||||
len(parsedCertificate.Subject.PostalCode) > 0 || len(parsedCertificate.Subject.SerialNumber) > 0 {
|
||||
err = core.InternalServerError("Generated certificate Subject contains fields other than CommonName or Names")
|
||||
return
|
||||
}
|
||||
if parsedCertificate.NotAfter.After(earliestExpiry) {
|
||||
err = core.InternalServerError("Generated certificate expires before earliest expiration")
|
||||
return
|
||||
}
|
||||
now := ra.clk.Now()
|
||||
if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 {
|
||||
err = core.InternalServerError(fmt.Sprintf("Generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore)))
|
||||
return
|
||||
}
|
||||
if !parsedCertificate.BasicConstraintsValid {
|
||||
err = core.InternalServerError("Generated certificate doesn't have basic constraints set")
|
||||
return
|
||||
}
|
||||
if parsedCertificate.IsCA {
|
||||
err = core.InternalServerError("Generated certificate can sign other certificates")
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
|
||||
err = core.InternalServerError("Generated certificate doesn't have correct key usage extensions")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewCertificate requests the issuance of a certificate.
|
||||
func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest, regID int64) (cert core.Certificate, err error) {
|
||||
emptyCert := core.Certificate{}
|
||||
|
|
@ -306,7 +391,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest,
|
|||
return emptyCert, err
|
||||
}
|
||||
|
||||
err = cert.MatchesCSR(csr, earliestExpiry)
|
||||
err = ra.MatchesCSR(cert, csr, earliestExpiry)
|
||||
if err != nil {
|
||||
logEvent.Error = err.Error()
|
||||
return emptyCert, err
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
||||
|
|
@ -19,6 +20,14 @@ func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
|
|||
m.SetReply(r)
|
||||
m.Compress = false
|
||||
|
||||
// Normally this test DNS server will return 127.0.0.1 for everything.
|
||||
// However, in some situations (for instance Docker), it's useful to return a
|
||||
// different hardcoded host. You can do so by setting the FAKE_DNS environment
|
||||
// variable.
|
||||
fakeDNS := os.Getenv("FAKE_DNS")
|
||||
if fakeDNS == "" {
|
||||
fakeDNS = "127.0.0.1"
|
||||
}
|
||||
for _, q := range r.Question {
|
||||
fmt.Printf("dns-srv: Query -- [%s] %s\n", q.Name, dns.TypeToString[q.Qtype])
|
||||
switch q.Qtype {
|
||||
|
|
@ -30,7 +39,7 @@ func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
|
|||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
}
|
||||
record.A = net.ParseIP("127.0.0.1")
|
||||
record.A = net.ParseIP(fakeDNS)
|
||||
|
||||
m.Answer = append(m.Answer, record)
|
||||
case dns.TypeMX:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Build and run a docker image for Boulder. This is suitable for running
|
||||
# repeatedly during development because Docker will cache the image it builds,
|
||||
# and will only re-do the minimum necessary.
|
||||
#
|
||||
# NOTE: Currently we're not able to effectively cache the DB setup steps,
|
||||
# because setting up the DB depends on source files in the Boulder repo. So any
|
||||
# time source files change, Docker treats that as potentially invalidating the
|
||||
# steps that came after the COPY. In theory we could add a step that copies only
|
||||
# the files necessary to do the migrations, run them, and then copy the rest of
|
||||
# the source.
|
||||
set -o errexit
|
||||
cd $(dirname $0)/..
|
||||
|
||||
# In order to talk to a letsencrypt client running on the host, the fake DNS
|
||||
# client used in Boulder's start.py needs to know what the host's IP is from the
|
||||
# perspective of the container. We try to figure it out automatically. If you'd
|
||||
# like your Boulder instance to always talk to some other host, you can set
|
||||
# FAKE_DNS to that host's IP address.
|
||||
if [ -z "${FAKE_DNS}" ] ; then
|
||||
FAKE_DNS=$(ifconfig docker0 | sed -n 's/ *inet addr:\([0-9.]\+\).*/\1/p')
|
||||
fi
|
||||
docker build --tag boulder .
|
||||
# The -i command makes the instance interactive, so you can kill start.py with Ctrl-C.
|
||||
docker run \
|
||||
--interactive \
|
||||
--tty \
|
||||
--rm=true \
|
||||
--publish 4000-4001:4000-4001 \
|
||||
--publish 8000-8100:8000-8100 \
|
||||
--env FAKE_DNS="${FAKE_DNS}" \
|
||||
boulder
|
||||
|
|
@ -1,28 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaqzue57mgXEoG
|
||||
TZZoVkkCZraebWgXI8irX2BgQB1A3iZa9onxGPMcWQMxhSuUisbEJi4UkMcVST12
|
||||
HX01rUwhj41UuBxJvI1w4wvdstssTAaa9c9tsQ5+UED2bFRL1MsyBdbmCF/+pu3i
|
||||
+ZIYqWgiKbjVBe3nlAVbo77zizwp3Y4Tp1/TBOwTAuFkHePmkNT63uPm9My/hNzs
|
||||
Sm1o+Q519Cf7ry+JQmOVgz/jIgFVGFYJ17EV3KUIpUuDShuyCFATBQspgJSN2DoX
|
||||
RUlQjXXkNTj23OxxdT/cVLcLJjytyG6e5izME2R2aCkDBWIc1a4/sRJ0R396auPX
|
||||
G6KhJ7o/AgMBAAECggEAC7GkmVgVzc0Mf7uAArV7YaYYapQFCbLX6jUU2VIfpBbn
|
||||
uXroZQUo5FzKhAT4jYuMiaoFU+K6Wp6l+fcyz0sh9WugGOaupNiPrRhNfl6WeZvp
|
||||
5+9r1nRLjztMHhWErhMRpd+RJuU9NMi0NbP+2sR8LhEPe3OuUBL98LbJqio9y0Bp
|
||||
8+JxmATL7vTvORMMamt80bDRGLBzoGySWZ0lEWwTuv8RjiCqO85YmHd9BmnTpOUn
|
||||
siYfEDZ9t+EF6NgJaISMLmcbzxZI5+aPIvfc1TwzDA96LUccGwvAR4qZ/+eYsG2I
|
||||
Iwn1wMMSsvFWeq7AccKL/povPg1Iud0wzkLLZmMjGQKBgQDLRUtENxbVH/T4xw8W
|
||||
tdgfzddQWZmVPtQXHAt2/zmrgJ3DKu7HOUajCGGWdLnoiFrIO2LsQYpP1XUA4hG1
|
||||
vzLDBng6SHDqZdGfd8eFaHesafr/FG4zkk+GBIs+P8GzHs2/znpEiqLnPbWrrB3t
|
||||
2/zBIXluZSytGt1ysC+TZZFwhQKBgQDCymmryD0JRV0M1IgwXOZrO+2MWDdliujx
|
||||
uBfD6DdLCJ5wHrPiySjVvl0njPLDQcWigigJD8Jl0SRQ8JSbceFzEGjzEj3NR2oB
|
||||
aLn3Gf3nORo7tJdCZVQn7QcHT+HzAWffm6qjqKzPXjRAEFFcW48Bnw+BU/JuZNOe
|
||||
v7dTuyv88wKBgFROGAphwsF/8I0hmht0Lf/60ltL3gvtM++lvQeMkTGVNVlVvBS6
|
||||
p5ZEipzpKpXLv8MeBkgwYpn70PwdxvSXKQmD7GdX1iURN6CpAAJPspq6ldQneBFB
|
||||
lGPkDJAzxzVwCCuOCl3VFf1MNcXOq9cUDz9Wj9N+eMoOw1umwQSj8m81AoGAP/li
|
||||
gzycbzMMwG383IVmV8my1ukSKJNatiiUBY96uXX3MzOiONWAR9LhnV+5S0+KrTi6
|
||||
FV/LpMzvdHXPGM5qEPROw6Y2DflqY1QV34X10b77UqiZFQFahlJegJRHzRulFdd2
|
||||
T5HST7jMyE2TqxWW/h1TZlI/yOnsZrLobuOGKukCgYAGk/TRPSOWEkH3rL7cDJFp
|
||||
ql4d33iuW9b3dMvQgkDW8H2kj3QA5os9dtSFLA/fWubULXOPpOsA7Ny7okBxsUBW
|
||||
tn4ER+HjUHogGir5d5cBBpvi4xM2/B4KaZnX8IHYhFcT42eb1oYmpfz5GcRyqzTX
|
||||
OoetYnUS5t4QuAAMadjtig==
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD3tBSSz5+lz4if
|
||||
t0imYGEps8RJyuNZ/aCOcE+tOb1nRXbu3DYxmY+6/BWl7P/bbWS5dKiMGUELiaIV
|
||||
qwnqXmoqiMkPLXcVyXClMloThoNked+vmXzDbwRFp0GWzrCl4q4UWk2wAn70p/XF
|
||||
tDxW5lJx6XNw9EcX3RvpqqZ46jsXY2MGDRD2xQLzZFUxS+nfLOrk5akan50iGqIv
|
||||
tbzesGySO5xTFrwg6sQIJCzkVIwCaCeIeS2ffFe7+MtPDUlXXZNvQ0WfG7iabPJZ
|
||||
pvJuNqPtVCCxSav4137b8ymn3F5UaPcYBjosZqK0btDykOkfLfiBHE1V/0xSGQ/U
|
||||
su55fGKPAgMBAAECggEBAJq6qKdUfptq2iM6sG0Ng2QzYQffbslF/0c6rOq+SKv7
|
||||
3mEyub+d9wSMd1FgjCqbWPHoB2kMVW9JmDAE2XrHY3YSX9XVRrAbVvw6Sb4spmLb
|
||||
JnlgXWkPLhH8eqIjloDo6mjYR/MMfBWD3aiT/6Ug1ptFgtEqJkan3cnGyZp6ZlMi
|
||||
jZQOoPyxaSVtYiyKZo7Mng02fwbkjiCQpTKCKk+dUnDQHmPI7kV69/jvqzJzCPwf
|
||||
IWXzfg9vhTuLjiGHNTfVqiEevSkmrEerVVIaC9/UNACkyd2jW+Hhl5lFRVRQBnIc
|
||||
8DmSVQZrFu08LNLlZOBy+kga/JTWv1s8Gb4WXkN70FECgYEA/3bs3hkUDF9/KnKC
|
||||
ljceKH05/G8cU84riqwt8lepjwu0Zzq+OVm58CMwljPRi2nuBGsFirFsmKMbAxM7
|
||||
QjGzcmCp6QEfm0rSKMs68BnmVyTbVoov4l9CNeWiGtRxSHvKXOAcKEMw032cKmeE
|
||||
f6NbVXzsaGglaDAjqFBPiQjoVMUCgYEA+Dj9n6QiAy3jF+XqlnjszF+2gPmutBDR
|
||||
bWpLvcJsa2aeuZq4Uw/42d+GneSDYY2O2e51WYwWJU7QvVvKrosDc8vrmq0YDt0S
|
||||
DsWknVnUImFJVgM/ssepQvDoheR03GB/EiRjSzl9duopi/9I2I6L2Bz0eHWdoe9I
|
||||
59W7NCbWl0MCgYA3jPnG9fcZFa8GYO4qkgO51DlGvjaPtrZbKkO3ff59/5KUG/7/
|
||||
Y5Z+NHi9QLlhMoCybz8+QISX0GfwTD9Hjp0x0vk/lKH119hJTKAdU9R1wKr9b4Eo
|
||||
HYS9SZjtcqLLmzeO6KDW1H2Kj7bktavnQXN77HVP4s23kTg/wm75Inm67QKBgQCc
|
||||
7KqqukEmcWQYP6pG5rwNmJTwhOkFvXwpCAJKPRf97ip6fG37VSWl18JrH4RPsAaX
|
||||
kEFwEzgM7f7ZN6azIf9UvZ59cuC6xNgdrqWbNKb/9TE/x14F0GQZheP6gau/huHO
|
||||
vmJntDi1bORHNWUAQ131ipijbvfw4C3fIyfumH8hsQKBgQDggOcVSKmMilITeEls
|
||||
Ui4K4dQ2feMyCv07LBe2zvrL4EP48dQw40DJKFj8HYPpqmFtjESjYnPw+9YUid5N
|
||||
fnssdGmnScfHUhZMoXIhmiEFrsAMDllwftqESi7+QRIie4Owftdpb/8VEyBH7xga
|
||||
MvhT37SxXefJCluspTw7n+am8g==
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
|
|||
|
|
@ -658,7 +658,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
|
|||
// 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"
|
||||
logEvent.Error = "Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it."
|
||||
wfe.log.Debug("Key mismatch for revoke")
|
||||
wfe.sendError(response,
|
||||
logEvent.Error,
|
||||
|
|
|
|||
|
|
@ -574,8 +574,18 @@ func TestIssueCertificate(t *testing.T) {
|
|||
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
|
||||
mockLog := wfe.log.SyslogWriter.(*mocks.MockSyslogWriter)
|
||||
|
||||
// TODO: Use a mock RA so we can test various conditions of authorized, not authorized, etc.
|
||||
ra := ra.NewRegistrationAuthorityImpl(clock.NewFake(), wfe.log)
|
||||
// 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
|
||||
// mock (see below), that date would trigger failures for excessive
|
||||
// backdating. So we set the fakeClock's time to a time that matches that test
|
||||
// certificate.
|
||||
fakeClock := clock.NewFake()
|
||||
testTime := time.Date(2015, 9, 9, 22, 56, 0, 0, time.UTC)
|
||||
fakeClock.Add(fakeClock.Now().Sub(testTime))
|
||||
|
||||
// TODO: Use a mock RA so we can test various conditions of authorized, not
|
||||
// authorized, etc.
|
||||
ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log)
|
||||
ra.SA = &MockSA{}
|
||||
ra.CA = &MockCA{}
|
||||
ra.PA = &MockPA{}
|
||||
|
|
@ -866,8 +876,32 @@ func TestNewRegistration(t *testing.T) {
|
|||
test.AssertEquals(t, responseWriter.Code, 409)
|
||||
}
|
||||
|
||||
// Valid revocation request for existing, non-revoked cert
|
||||
func TestRevokeCertificate(t *testing.T) {
|
||||
func makeRevokeRequestJSON() ([]byte, error) {
|
||||
certPemBytes, err := ioutil.ReadFile("test/238.crt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certBlock, _ := pem.Decode(certPemBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
revokeRequest := struct {
|
||||
Resource string `json:"resource"`
|
||||
CertificateDER core.JSONBuffer `json:"certificate"`
|
||||
}{
|
||||
Resource: "revoke-cert",
|
||||
CertificateDER: certBlock.Bytes,
|
||||
}
|
||||
revokeRequestJSON, err := json.Marshal(revokeRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return revokeRequestJSON, nil
|
||||
}
|
||||
|
||||
// Valid revocation request for existing, non-revoked cert, signed with cert
|
||||
// key.
|
||||
func TestRevokeCertificateCertKey(t *testing.T) {
|
||||
keyPemBytes, err := ioutil.ReadFile("test/238.key")
|
||||
test.AssertNotError(t, err, "Failed to load key")
|
||||
key, err := jose.LoadPrivateKey(keyPemBytes)
|
||||
|
|
@ -877,29 +911,12 @@ func TestRevokeCertificate(t *testing.T) {
|
|||
signer, err := jose.NewSigner("RS256", rsaKey)
|
||||
test.AssertNotError(t, err, "Failed to make signer")
|
||||
|
||||
certPemBytes, err := ioutil.ReadFile("test/238.crt")
|
||||
test.AssertNotError(t, err, "Failed to load cert")
|
||||
certBlock, _ := pem.Decode(certPemBytes)
|
||||
test.Assert(t, certBlock != nil, "Failed to decode PEM")
|
||||
revokeRequest := struct {
|
||||
Resource string `json:"resource"`
|
||||
CertificateDER core.JSONBuffer `json:"certificate"`
|
||||
}{
|
||||
Resource: "revoke-cert",
|
||||
CertificateDER: certBlock.Bytes,
|
||||
}
|
||||
revokeRequestJSON, err := json.Marshal(revokeRequest)
|
||||
test.AssertNotError(t, err, "Failed to marshal request")
|
||||
revokeRequestJSON, err := makeRevokeRequestJSON()
|
||||
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
|
||||
|
||||
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
|
||||
wfe := setupWFE(t)
|
||||
|
||||
wfe.RA = &MockRegistrationAuthority{}
|
||||
wfe.SA = &MockSA{}
|
||||
wfe.Stats, _ = statsd.NewNoopClient()
|
||||
wfe.SubscriberAgreementURL = agreementURL
|
||||
responseWriter := httptest.NewRecorder()
|
||||
responseWriter.Body.Reset()
|
||||
|
||||
nonce, err := wfe.nonceService.Nonce()
|
||||
test.AssertNotError(t, err, "Unable to create nonce")
|
||||
result, _ := signer.Sign(revokeRequestJSON, nonce)
|
||||
|
|
@ -907,25 +924,57 @@ func TestRevokeCertificate(t *testing.T) {
|
|||
makePostRequest(result.FullSerialize()))
|
||||
test.AssertEquals(t, responseWriter.Code, 200)
|
||||
test.AssertEquals(t, responseWriter.Body.String(), "")
|
||||
}
|
||||
|
||||
// Valid revocation request for existing, non-revoked cert, signed with account
|
||||
// key.
|
||||
func TestRevokeCertificateAccountKey(t *testing.T) {
|
||||
revokeRequestJSON, err := makeRevokeRequestJSON()
|
||||
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
|
||||
|
||||
wfe := setupWFE(t)
|
||||
responseWriter := httptest.NewRecorder()
|
||||
|
||||
// Try the revoke request again, signed by account key associated with cert.
|
||||
// Should also succeed.
|
||||
responseWriter.Body.Reset()
|
||||
test1JWK, err := jose.LoadPrivateKey([]byte(test1KeyPrivatePEM))
|
||||
test.AssertNotError(t, err, "Failed to load key")
|
||||
test1Key, ok := test1JWK.(*rsa.PrivateKey)
|
||||
test.Assert(t, ok, "Couldn't load RSA key")
|
||||
accountKeySigner, err := jose.NewSigner("RS256", test1Key)
|
||||
test.AssertNotError(t, err, "Failed to make signer")
|
||||
nonce, err = wfe.nonceService.Nonce()
|
||||
nonce, err := wfe.nonceService.Nonce()
|
||||
test.AssertNotError(t, err, "Unable to create nonce")
|
||||
result, _ = accountKeySigner.Sign(revokeRequestJSON, nonce)
|
||||
result, _ := accountKeySigner.Sign(revokeRequestJSON, nonce)
|
||||
wfe.RevokeCertificate(responseWriter,
|
||||
makePostRequest(result.FullSerialize()))
|
||||
test.AssertEquals(t, responseWriter.Code, 200)
|
||||
test.AssertEquals(t, responseWriter.Body.String(), "")
|
||||
}
|
||||
|
||||
// A revocation request signed by an unauthorized key.
|
||||
func TestRevokeCertificateWrongKey(t *testing.T) {
|
||||
wfe := setupWFE(t)
|
||||
nonce, err := wfe.nonceService.Nonce()
|
||||
test.AssertNotError(t, err, "Unable to create nonce")
|
||||
responseWriter := httptest.NewRecorder()
|
||||
test2JWK, err := jose.LoadPrivateKey([]byte(test2KeyPrivatePEM))
|
||||
test.AssertNotError(t, err, "Failed to load key")
|
||||
test2Key, ok := test2JWK.(*rsa.PrivateKey)
|
||||
test.Assert(t, ok, "Couldn't load RSA key")
|
||||
accountKeySigner2, err := jose.NewSigner("RS256", test2Key)
|
||||
test.AssertNotError(t, err, "Failed to make signer")
|
||||
nonce, err = wfe.nonceService.Nonce()
|
||||
test.AssertNotError(t, err, "Unable to create nonce")
|
||||
revokeRequestJSON, err := makeRevokeRequestJSON()
|
||||
test.AssertNotError(t, err, "Unable to create revoke request")
|
||||
|
||||
result, _ := accountKeySigner2.Sign(revokeRequestJSON, nonce)
|
||||
wfe.RevokeCertificate(responseWriter,
|
||||
makePostRequest(result.FullSerialize()))
|
||||
test.AssertEquals(t, responseWriter.Code, 403)
|
||||
test.AssertEquals(t, responseWriter.Body.String(),
|
||||
`{"type":"urn:acme:error:unauthorized","detail":"Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it."}`)
|
||||
}
|
||||
|
||||
// Valid revocation request for already-revoked cert
|
||||
func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
|
||||
keyPemBytes, err := ioutil.ReadFile("test/178.key")
|
||||
|
|
|
|||
Loading…
Reference in New Issue