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
|
ENV BOULDER_CONFIG /go/src/github.com/letsencrypt/boulder/test/boulder-config.json
|
||||||
|
|
||||||
# Get the Let's Encrypt client
|
# 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
|
WORKDIR /letsencrypt
|
||||||
RUN ./bootstrap/debian.sh && \
|
RUN ./bootstrap/debian.sh && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
|
|
@ -43,20 +43,13 @@ RUN ./bootstrap/debian.sh && \
|
||||||
RUN virtualenv --no-site-packages -p python2 venv && \
|
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
|
./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
|
ENV LETSENCRYPT_PATH /letsencrypt
|
||||||
|
|
||||||
# Copy in the Boulder sources
|
# Copy in the Boulder sources
|
||||||
COPY . /go/src/github.com/letsencrypt/boulder
|
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
|
WORKDIR /go/src/github.com/letsencrypt/boulder
|
||||||
CMD ["bash", "-c", "service mysql start && \
|
CMD ["bash", "-c", "service mysql start && \
|
||||||
service rsyslog 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://travis-ci.org/letsencrypt/boulder)
|
||||||
[](https://coveralls.io/r/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.
|
Slow start
|
||||||
|
|
||||||
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
|
|
||||||
----------
|
----------
|
||||||
|
|
||||||
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
|
MariaDB 10 to work correctly. On Ubuntu and CentOS, you may have to
|
||||||
install RabbitMQ from https://rabbitmq.com/download.html to get a
|
install RabbitMQ from https://rabbitmq.com/download.html to get a
|
||||||
recent version.
|
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:
|
Ubuntu:
|
||||||
|
|
||||||
sudo apt-get install libltdl3-dev mariadb-server rabbitmq-server
|
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.)
|
(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
|
> go get github.com/letsencrypt/boulder/ # Ignore errors about no buildable files
|
||||||
> cd $GOPATH/src/github.com/letsencrypt/boulder
|
> cd $GOPATH/src/github.com/letsencrypt/boulder
|
||||||
|
> ./test/create_db.sh
|
||||||
# This starts each Boulder component with test configs. Ctrl-C kills all.
|
# This starts each Boulder component with test configs. Ctrl-C kills all.
|
||||||
|
> ./start.py
|
||||||
|
# Run tests
|
||||||
> ./test.sh
|
> ./test.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The databases that boulder requires to operate in development and
|
Note: create\_db.sh it uses the root MariaDB user, so if you
|
||||||
testing can be created using test/create\_db.sh. It uses the root
|
have disabled that account you may have to adjust the file or
|
||||||
MariaDB user, so if you have disabled that account you may have to
|
recreate the commands.
|
||||||
adjust the file or recreate the commands.
|
|
||||||
|
|
||||||
You can also check out the official client from
|
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.
|
instructions there.
|
||||||
|
|
||||||
Component Model
|
Component Model
|
||||||
|
|
@ -130,7 +114,7 @@ and to [avoid insecure fallback in go
|
||||||
get](https://github.com/golang/go/issues/9637).
|
get](https://github.com/golang/go/issues/9637).
|
||||||
|
|
||||||
Local development also requires a RabbitMQ installation and MariaDB
|
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.
|
default integration tests.
|
||||||
|
|
||||||
To update the Go dependencies:
|
To update the Go dependencies:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
@ -194,7 +195,7 @@ func (c *certChecker) checkCert(cert core.Certificate) (problems []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check the cert has the correct key usage extensions
|
// 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")
|
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
|
// DNSResolverImpl represents a client that talks to an external resolver
|
||||||
type DNSResolverImpl struct {
|
type DNSResolverImpl struct {
|
||||||
DNSClient *dns.Client
|
DNSClient *dns.Client
|
||||||
Servers []string
|
Servers []string
|
||||||
allowLoopbackAddresses bool
|
allowRestrictedAddresses bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSResolverImpl constructs a new DNS resolver object that utilizes the
|
// 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
|
dnsClient.DialTimeout = dialTimeout
|
||||||
|
|
||||||
return &DNSResolverImpl{
|
return &DNSResolverImpl{
|
||||||
DNSClient: dnsClient,
|
DNSClient: dnsClient,
|
||||||
Servers: servers,
|
Servers: servers,
|
||||||
allowLoopbackAddresses: false,
|
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).
|
// This constructor should *only* be called from tests (unit or integration).
|
||||||
func NewTestDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolverImpl {
|
func NewTestDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolverImpl {
|
||||||
resolver := NewDNSResolverImpl(dialTimeout, servers)
|
resolver := NewDNSResolverImpl(dialTimeout, servers)
|
||||||
resolver.allowLoopbackAddresses = true
|
resolver.allowRestrictedAddresses = true
|
||||||
return resolver
|
return resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,8 +120,8 @@ func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.D
|
||||||
return txt, rtt, err
|
return txt, rtt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPrivateV4(ip net.IP, allowLoopback bool) bool {
|
func isPrivateV4(ip net.IP) bool {
|
||||||
return rfc1918_10.Contains(ip) || rfc1918_172_16.Contains(ip) || rfc1918_192_168.Contains(ip) || (!allowLoopback && rfc5735_127.Contains(ip))
|
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
|
// 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 {
|
for _, answer := range r.Answer {
|
||||||
if answer.Header().Rrtype == dns.TypeA {
|
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)
|
addrs = append(addrs, a.A)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
123
core/objects.go
123
core/objects.go
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -101,52 +100,6 @@ func (pd *ProblemDetails) Error() string {
|
||||||
return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail)
|
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
|
// An AcmeIdentifier encodes an identifier that can
|
||||||
// be validated by ACME. The protocol allows for different
|
// be validated by ACME. The protocol allows for different
|
||||||
// types of identifier to be supported (DNS names, IP
|
// 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
|
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
|
// CertificateStatus structs are internal to the server. They represent the
|
||||||
// latest data about the status of the certificate, required for OCSP updating
|
// latest data about the status of the certificate, required for OCSP updating
|
||||||
// and for validating that the subscriber has accepted the certificate.
|
// and for validating that the subscriber has accepted the certificate.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -203,6 +205,89 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
|
||||||
return authz, err
|
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.
|
// NewCertificate requests the issuance of a certificate.
|
||||||
func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest, regID int64) (cert core.Certificate, err error) {
|
func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest, regID int64) (cert core.Certificate, err error) {
|
||||||
emptyCert := core.Certificate{}
|
emptyCert := core.Certificate{}
|
||||||
|
|
@ -306,7 +391,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest,
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cert.MatchesCSR(csr, earliestExpiry)
|
err = ra.MatchesCSR(cert, csr, earliestExpiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logEvent.Error = err.Error()
|
logEvent.Error = err.Error()
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
"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.SetReply(r)
|
||||||
m.Compress = false
|
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 {
|
for _, q := range r.Question {
|
||||||
fmt.Printf("dns-srv: Query -- [%s] %s\n", q.Name, dns.TypeToString[q.Qtype])
|
fmt.Printf("dns-srv: Query -- [%s] %s\n", q.Name, dns.TypeToString[q.Qtype])
|
||||||
switch q.Qtype {
|
switch q.Qtype {
|
||||||
|
|
@ -30,7 +39,7 @@ func dnsHandler(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
Class: dns.ClassINET,
|
Class: dns.ClassINET,
|
||||||
Ttl: 0,
|
Ttl: 0,
|
||||||
}
|
}
|
||||||
record.A = net.ParseIP("127.0.0.1")
|
record.A = net.ParseIP(fakeDNS)
|
||||||
|
|
||||||
m.Answer = append(m.Answer, record)
|
m.Answer = append(m.Answer, record)
|
||||||
case dns.TypeMX:
|
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-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
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
|
||||||
OoetYnUS5t4QuAAMadjtig==
|
MvhT37SxXefJCluspTw7n+am8g==
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|
|
||||||
|
|
@ -658,7 +658,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
|
||||||
// TODO: Implement method of revocation by authorizations on account.
|
// TODO: Implement method of revocation by authorizations on account.
|
||||||
if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) ||
|
if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) ||
|
||||||
registration.ID == cert.RegistrationID) {
|
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.log.Debug("Key mismatch for revoke")
|
||||||
wfe.sendError(response,
|
wfe.sendError(response,
|
||||||
logEvent.Error,
|
logEvent.Error,
|
||||||
|
|
|
||||||
|
|
@ -574,8 +574,18 @@ func TestIssueCertificate(t *testing.T) {
|
||||||
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
|
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
|
||||||
mockLog := wfe.log.SyslogWriter.(*mocks.MockSyslogWriter)
|
mockLog := wfe.log.SyslogWriter.(*mocks.MockSyslogWriter)
|
||||||
|
|
||||||
// TODO: Use a mock RA so we can test various conditions of authorized, not authorized, etc.
|
// The mock CA we use always returns the same test certificate, with a Not
|
||||||
ra := ra.NewRegistrationAuthorityImpl(clock.NewFake(), wfe.log)
|
// 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.SA = &MockSA{}
|
||||||
ra.CA = &MockCA{}
|
ra.CA = &MockCA{}
|
||||||
ra.PA = &MockPA{}
|
ra.PA = &MockPA{}
|
||||||
|
|
@ -866,8 +876,32 @@ func TestNewRegistration(t *testing.T) {
|
||||||
test.AssertEquals(t, responseWriter.Code, 409)
|
test.AssertEquals(t, responseWriter.Code, 409)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid revocation request for existing, non-revoked cert
|
func makeRevokeRequestJSON() ([]byte, error) {
|
||||||
func TestRevokeCertificate(t *testing.T) {
|
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")
|
keyPemBytes, err := ioutil.ReadFile("test/238.key")
|
||||||
test.AssertNotError(t, err, "Failed to load key")
|
test.AssertNotError(t, err, "Failed to load key")
|
||||||
key, err := jose.LoadPrivateKey(keyPemBytes)
|
key, err := jose.LoadPrivateKey(keyPemBytes)
|
||||||
|
|
@ -877,29 +911,12 @@ func TestRevokeCertificate(t *testing.T) {
|
||||||
signer, err := jose.NewSigner("RS256", rsaKey)
|
signer, err := jose.NewSigner("RS256", rsaKey)
|
||||||
test.AssertNotError(t, err, "Failed to make signer")
|
test.AssertNotError(t, err, "Failed to make signer")
|
||||||
|
|
||||||
certPemBytes, err := ioutil.ReadFile("test/238.crt")
|
revokeRequestJSON, err := makeRevokeRequestJSON()
|
||||||
test.AssertNotError(t, err, "Failed to load cert")
|
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
|
||||||
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")
|
|
||||||
|
|
||||||
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
|
|
||||||
wfe := setupWFE(t)
|
wfe := setupWFE(t)
|
||||||
|
|
||||||
wfe.RA = &MockRegistrationAuthority{}
|
|
||||||
wfe.SA = &MockSA{}
|
|
||||||
wfe.Stats, _ = statsd.NewNoopClient()
|
|
||||||
wfe.SubscriberAgreementURL = agreementURL
|
|
||||||
responseWriter := httptest.NewRecorder()
|
responseWriter := httptest.NewRecorder()
|
||||||
responseWriter.Body.Reset()
|
|
||||||
nonce, err := wfe.nonceService.Nonce()
|
nonce, err := wfe.nonceService.Nonce()
|
||||||
test.AssertNotError(t, err, "Unable to create nonce")
|
test.AssertNotError(t, err, "Unable to create nonce")
|
||||||
result, _ := signer.Sign(revokeRequestJSON, nonce)
|
result, _ := signer.Sign(revokeRequestJSON, nonce)
|
||||||
|
|
@ -907,25 +924,57 @@ func TestRevokeCertificate(t *testing.T) {
|
||||||
makePostRequest(result.FullSerialize()))
|
makePostRequest(result.FullSerialize()))
|
||||||
test.AssertEquals(t, responseWriter.Code, 200)
|
test.AssertEquals(t, responseWriter.Code, 200)
|
||||||
test.AssertEquals(t, responseWriter.Body.String(), "")
|
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))
|
test1JWK, err := jose.LoadPrivateKey([]byte(test1KeyPrivatePEM))
|
||||||
test.AssertNotError(t, err, "Failed to load key")
|
test.AssertNotError(t, err, "Failed to load key")
|
||||||
test1Key, ok := test1JWK.(*rsa.PrivateKey)
|
test1Key, ok := test1JWK.(*rsa.PrivateKey)
|
||||||
test.Assert(t, ok, "Couldn't load RSA key")
|
test.Assert(t, ok, "Couldn't load RSA key")
|
||||||
accountKeySigner, err := jose.NewSigner("RS256", test1Key)
|
accountKeySigner, err := jose.NewSigner("RS256", test1Key)
|
||||||
test.AssertNotError(t, err, "Failed to make signer")
|
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")
|
test.AssertNotError(t, err, "Unable to create nonce")
|
||||||
result, _ = accountKeySigner.Sign(revokeRequestJSON, nonce)
|
result, _ := accountKeySigner.Sign(revokeRequestJSON, nonce)
|
||||||
wfe.RevokeCertificate(responseWriter,
|
wfe.RevokeCertificate(responseWriter,
|
||||||
makePostRequest(result.FullSerialize()))
|
makePostRequest(result.FullSerialize()))
|
||||||
test.AssertEquals(t, responseWriter.Code, 200)
|
test.AssertEquals(t, responseWriter.Code, 200)
|
||||||
test.AssertEquals(t, responseWriter.Body.String(), "")
|
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
|
// Valid revocation request for already-revoked cert
|
||||||
func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
|
func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
|
||||||
keyPemBytes, err := ioutil.ReadFile("test/178.key")
|
keyPemBytes, err := ioutil.ReadFile("test/178.key")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue