Merge master

This commit is contained in:
Roland Shoemaker 2015-08-11 16:39:31 -07:00
commit 8789f925cc
138 changed files with 9238 additions and 2696 deletions

2
.gitignore vendored
View File

@ -2,6 +2,7 @@
*.o
*.a
*.so
*.pyc
# Folders
_obj
@ -11,6 +12,7 @@ bin
# Test files
test/js/node_modules
test/js/*.pem
test/github-secret.json
# Architecture specific extensions/prefixes
*.[568vq]

View File

@ -3,11 +3,26 @@ language: go
go:
- 1.4.1
addons:
hosts:
- le.wtf
apt:
packages:
- lsb-release
- python-dev
- python-virtualenv
- gcc
- libaugeas0
- libssl-dev
- libffi-dev
- ca-certificates
- rsyslog
sudo: false
services:
- rabbitmq
sudo: required
matrix:
fast_finish: true
@ -16,14 +31,17 @@ branches:
only:
- master
sudo: required
before_install:
# Github-PR-Status secret
- openssl aes-256-cbc -K $encrypted_53b2630f0fb4_key -iv $encrypted_53b2630f0fb4_iv -in test/github-secret.json.enc -out test/github-secret.json -d || true
- go get golang.org/x/tools/cmd/vet
- go get golang.org/x/tools/cmd/cover
- go get github.com/golang/lint/golint
- go get github.com/mattn/goveralls
- go get github.com/modocache/gover
- go get github.com/jcjones/github-pr-status
# Boulder consists of multiple Go packages, which
# refer to each other by their absolute GitHub path,
# e.g. github.com/letsencrypt/boulder/analysis. That means, by default, if
@ -31,15 +49,14 @@ before_install:
# we add a symlink.
- mkdir -p $TRAVIS_BUILD_DIR $GOPATH/src/github.com/letsencrypt
- test ! -d $GOPATH/src/github.com/letsencrypt/boulder && ln -s $TRAVIS_BUILD_DIR $GOPATH/src/github.com/letsencrypt/boulder || true
- git clone https://www.github.com/letsencrypt/lets-encrypt-preview.git /tmp/letsencrypt
- cd /tmp/letsencrypt
- sudo ./bootstrap/debian.sh
- git clone https://www.github.com/letsencrypt/lets-encrypt-preview.git "$LETSENCRYPT_PATH"
- cd "$LETSENCRYPT_PATH"
- virtualenv --no-site-packages -p python2 ./venv
- travis_retry ./venv/bin/pip install -r requirements.txt -e .
- travis_retry ./venv/bin/pip install -r requirements.txt -e acme -e . -e letsencrypt-apache -e letsencrypt-nginx
- "cd -"
env:
- LETSENCRYPT_VENV=/tmp/letsencrypt/venv
- LETSENCRYPT_PATH=/tmp/letsencrypt
script:
- make -j4 # Travis has 2 cores per build instance

View File

@ -1,8 +1,7 @@
# Boulder flow diagrams
Boulder is built in a rather decentralized way in order to enable different
parts to be deployed in different security contexts. (Of course, they can
also be run together, as in `./cmd/boulder`.)
parts to be deployed in different security contexts.
In order to you understand how boulder works and ensure it's working correctly,
this document lays out how various operations flow through boulder. We show a
@ -18,9 +17,7 @@ A couple of notes:
(certificates), and read by WFE, RA, and CA.
* The interactions shown in the diagrams are the calls that go between
components. These calls can be done directly (as in `./cmd/boulder`), or
they can be done via the AMQP-based RPC code in `./rpc/`. We do not
distinguish between those cases here.
components. These calls are done via the AMQP-based RPC code in `./rpc/`.
## New Registration
@ -168,11 +165,9 @@ Notes:
1: Client ---new-cert--> WFE
2: WFE ---NewCertificate--> RA
3: RA ---IssueCertificate--> CA
4: CA --> CFSSL
5: CA <-- CFSSL
6: RA <------return--------- CA
7: WFE <------return------- RA
8: Client <------------- WFE
5: RA <------return--------- CA
5: WFE <------return------- RA
6: Client <------------- WFE
```
* 1-2: WFE does the following:
@ -205,7 +200,8 @@ Notes:
* Verify that the issued cert will not be valid longer than the CA cert
* Verify that the issued cert will not be valid longer than the underlying authorizations
* Open a CA DB transaction and allocate a new serial number
* Request that CFSSL sign the certificate
* Create the first OCSP response
* Sign the certificate and the first OCSP response with the CFSSL library
* 5-6: CA does the following:
* Store the certificate

View File

@ -40,15 +40,14 @@ RUN ./bootstrap/debian.sh && \
/tmp/* \
/var/tmp/*
RUN virtualenv --no-site-packages -p python2 venv && \
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]
./venv/bin/pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx
# Copy in the Boulder sources
COPY . /go/src/github.com/letsencrypt/boulder
# Build Boulder
RUN go install -tags pkcs11 \
RUN go install \
github.com/letsencrypt/boulder/cmd/activity-monitor \
github.com/letsencrypt/boulder/cmd/boulder \
github.com/letsencrypt/boulder/cmd/boulder-ca \
github.com/letsencrypt/boulder/cmd/boulder-ra \
github.com/letsencrypt/boulder/cmd/boulder-sa \

34
Godeps/Godeps.json generated
View File

@ -12,51 +12,51 @@
},
{
"ImportPath": "github.com/cloudflare/cfssl/auth",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/config",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs11key",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs12",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs7",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/csr",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/errors",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/helpers",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/info",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/log",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/ocsp",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/cloudflare/cfssl/signer",
"Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a"
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
},
{
"ImportPath": "github.com/codegangsta/cli",
@ -72,21 +72,25 @@
"Comment": "v1.2-88-ga197e5d",
"Rev": "a197e5d40516f2e9f74dcee085a5f2d4604e94df"
},
{
"ImportPath": "github.com/letsencrypt/go-jose",
"Rev": "e7bd87a386998d423741e8e370af1a22638767e0"
},
{
"ImportPath": "github.com/mattn/go-sqlite3",
"Rev": "308067797b0fcce4ca06362580dc6db77c1bfeda"
},
{
"ImportPath": "github.com/miekg/dns",
"Rev": "6da0cd2c927d5cb11255c468b5c3a1744c3351b1"
"Rev": "259969e797348d20e8c144a7573c23f06fa962f5"
},
{
"ImportPath": "github.com/miekg/pkcs11",
"Rev": "88c9f842544e629ec046105d7fb50d5daafae737"
},
{
"ImportPath": "github.com/square/go-jose",
"Rev": "d3ba9be3fbf631c353e477ab8fba8ec04f05a8b4"
"ImportPath": "github.com/square/go-jose/cipher",
"Rev": "2f4e4fff85a98acc176ac2712c31394ac1acb7e8"
},
{
"ImportPath": "github.com/streadway/amqp",

View File

@ -0,0 +1,19 @@
Copyright (c) 2012-2015 Eli Janssen
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,24 @@
Copyright (c) 2014 CloudFlare Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -38,38 +38,46 @@ type CSRWhitelist struct {
// JSON marshal / unmarshal.
type OID asn1.ObjectIdentifier
// CertificatePolicy is a flattening of the ASN.1 PolicyInformation structure from
// CertificatePolicy represents the ASN.1 PolicyInformation structure from
// https://tools.ietf.org/html/rfc3280.html#page-106.
// Valid values of Type are "id-qt-unotice" and "id-qt-cps"
type CertificatePolicy struct {
ID OID
Type string
Qualifier string
ID OID
Qualifiers []CertificatePolicyQualifier
}
// CertificatePolicyQualifier represents a single qualifier from an ASN.1
// PolicyInformation structure.
type CertificatePolicyQualifier struct {
Type string
Value string
}
// A SigningProfile stores information that the CA needs to store
// signature policy.
type SigningProfile struct {
Usage []string `json:"usages"`
IssuerURL []string `json:"issuer_urls"`
OCSP string `json:"ocsp_url"`
CRL string `json:"crl_url"`
CA bool `json:"is_ca"`
OCSPNoCheck bool `json:"ocsp_no_check"`
ExpiryString string `json:"expiry"`
BackdateString string `json:"backdate"`
AuthKeyName string `json:"auth_key"`
RemoteName string `json:"remote"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
Usage []string `json:"usages"`
IssuerURL []string `json:"issuer_urls"`
OCSP string `json:"ocsp_url"`
CRL string `json:"crl_url"`
CA bool `json:"is_ca"`
OCSPNoCheck bool `json:"ocsp_no_check"`
ExpiryString string `json:"expiry"`
BackdateString string `json:"backdate"`
AuthKeyName string `json:"auth_key"`
RemoteName string `json:"remote"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
NameWhitelistString string `json:"name_whitelist"`
Policies []CertificatePolicy
Expiry time.Duration
Backdate time.Duration
Provider auth.Provider
RemoteServer string
UseSerialSeq bool
CSRWhitelist *CSRWhitelist
Policies []CertificatePolicy
Expiry time.Duration
Backdate time.Duration
Provider auth.Provider
RemoteServer string
UseSerialSeq bool
CSRWhitelist *CSRWhitelist
NameWhitelist *regexp.Regexp
}
// UnmarshalJSON unmarshals a JSON string into an OID.
@ -162,8 +170,11 @@ func (p *SigningProfile) populate(cfg *Config) error {
if len(p.Policies) > 0 {
for _, policy := range p.Policies {
if policy.Type != "" && policy.Type != "id-qt-unotice" && policy.Type != "id-qt-cps" {
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err)
for _, qualifier := range policy.Qualifiers {
if qualifier.Type != "" && qualifier.Type != "id-qt-unotice" && qualifier.Type != "id-qt-cps" {
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
errors.New("invalid policy qualifier type"))
}
}
}
}
@ -200,6 +211,16 @@ func (p *SigningProfile) populate(cfg *Config) error {
}
}
if p.NameWhitelistString != "" {
log.Debug("compiling whitelist regular expression")
rule, err := regexp.Compile(p.NameWhitelistString)
if err != nil {
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
errors.New("failed to compile name whitelist section"))
}
p.NameWhitelist = rule
}
return nil
}
@ -370,6 +391,7 @@ var KeyUsage = map[string]x509.KeyUsage{
"digital signature": x509.KeyUsageDigitalSignature,
"content committment": x509.KeyUsageContentCommitment,
"key encipherment": x509.KeyUsageKeyEncipherment,
"key agreement": x509.KeyUsageKeyAgreement,
"data encipherment": x509.KeyUsageDataEncipherment,
"cert sign": x509.KeyUsageCertSign,
"crl sign": x509.KeyUsageCRLSign,

View File

@ -1,4 +1,4 @@
// +build pkcs11
// +build !nopkcs11
// Package pkcs11key implements crypto.Signer for PKCS #11 private
// keys. Currently, only RSA keys are support.
@ -131,7 +131,10 @@ func New(module, slot, pin, privLabel string) (ps *PKCS11Key, err error) {
ps.Destroy()
return
}
ps.publicKey = rsa.PublicKey{n, e}
ps.publicKey = rsa.PublicKey{
N: n,
E: e,
}
return
}

View File

@ -1,8 +1,7 @@
// Package pkcs11 in the ocsp directory provides a way to construct a
// PKCS#11-based OCSP signer. It is only available in binaries built with the
// pkcs11 tag, i.e. `go build -tags pkcs11 ./cmd/cfssl`.
// +build pkcs11
// +build !nopkcs11
// Package pkcs11 in the ocsp directory provides a way to construct a
// PKCS#11-based OCSP signer.
package pkcs11
import (
@ -18,7 +17,7 @@ import (
// Enabled is set to true if PKCS #11 support is present.
const Enabled = true
// New returns a new PKCS #11 signer.
// NewPKCS11Signer returns a new PKCS #11 signer.
func NewPKCS11Signer(cfg ocspConfig.Config) (ocsp.Signer, error) {
log.Debugf("Loading PKCS #11 module %s", cfg.PKCS11.Module)
certData, err := ioutil.ReadFile(cfg.CACertFile)

View File

@ -1,4 +1,4 @@
// +build !pkcs11
// +build nopkcs11
package pkcs11

View File

@ -225,6 +225,20 @@ func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) {
OverrideHosts(&safeTemplate, req.Hosts)
safeTemplate.Subject = PopulateSubjectFromCSR(req.Subject, safeTemplate.Subject)
// If there is a whitelist, ensure that both the Common Name and SAN DNSNames match
if profile.NameWhitelist != nil {
if safeTemplate.Subject.CommonName != "" {
if profile.NameWhitelist.Find([]byte(safeTemplate.Subject.CommonName)) == nil {
return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
}
}
for _, name := range safeTemplate.DNSNames {
if profile.NameWhitelist.Find([]byte(name)) == nil {
return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
}
}
}
return s.sign(&safeTemplate, profile, serialSeq)
}

View File

@ -6,6 +6,7 @@ import (
"encoding/pem"
"io/ioutil"
"reflect"
"regexp"
"sort"
"strings"
"testing"
@ -807,3 +808,75 @@ func TestWhitelistSign(t *testing.T) {
cert.SignatureAlgorithm)
}
}
func TestNameWhitelistSign(t *testing.T) {
csrPEM, err := ioutil.ReadFile(fullSubjectCSR)
if err != nil {
t.Fatalf("%v", err)
}
subInvalid := &signer.Subject{
CN: "localhost.com",
}
subValid := &signer.Subject{
CN: "1lab41.cf",
}
wl := regexp.MustCompile("^1[a-z]*[0-9]*\\.cf$")
s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile)
// Whitelist only key-related fields. Subject, DNSNames, etc shouldn't get
// passed through from CSR.
s.policy = &config.Signing{
Default: &config.SigningProfile{
Usage: []string{"cert sign", "crl sign"},
ExpiryString: "1h",
Expiry: 1 * time.Hour,
CA: true,
NameWhitelist: wl,
},
}
request := signer.SignRequest{
Hosts: []string{"127.0.0.1", "1machine23.cf"},
Request: string(csrPEM),
}
_, err = s.Sign(request)
if err != nil {
t.Fatalf("%v", err)
}
request = signer.SignRequest{
Hosts: []string{"invalid.cf", "1machine23.cf"},
Request: string(csrPEM),
}
_, err = s.Sign(request)
if err == nil {
t.Fatalf("expected a policy error")
}
request = signer.SignRequest{
Hosts: []string{"1machine23.cf"},
Request: string(csrPEM),
Subject: subInvalid,
}
_, err = s.Sign(request)
if err == nil {
t.Fatalf("expected a policy error")
}
request = signer.SignRequest{
Hosts: []string{"1machine23.cf"},
Request: string(csrPEM),
Subject: subValid,
}
_, err = s.Sign(request)
if err != nil {
t.Fatalf("%v", err)
}
}

View File

@ -1,4 +1,4 @@
// +build pkcs11
// +build !nopkcs11
package pkcs11

View File

@ -1,4 +1,4 @@
// +build !pkcs11
// +build nopkcs11
package pkcs11

View File

@ -83,9 +83,9 @@ func (s *Signer) remoteOp(req interface{}, profile, target string) (resp interfa
if target == "info" {
resp, err = server.Info(jsonData)
} else if p.Provider != nil {
resp, err = server.AuthReq(jsonData, nil, p.Provider, target)
resp, err = server.AuthSign(jsonData, nil, p.Provider)
} else {
resp, err = server.Req(jsonData, target)
resp, err = server.Sign(jsonData)
}
if err != nil {

View File

@ -27,19 +27,11 @@ import (
// MaxPathLen is the default path length for a new CA certificate.
var MaxPathLen = 2
// A Whitelist marks which fields should be set. As a bool's default
// value is false, a whitelist should only keep those fields marked
// true.
type Whitelist struct {
CN, C, ST, L, O, OU bool
}
// Subject contains the information that should be used to override the
// subject information when signing a certificate.
type Subject struct {
CN string
Names []csr.Name `json:"names"`
Whitelist *Whitelist `json:"whitelist,omitempty"`
CN string
Names []csr.Name `json:"names"`
}
// SignRequest stores a signature request, which contains the hostname,
@ -351,13 +343,26 @@ func FillTemplate(template *x509.Certificate, defaultProfile, profile *config.Si
return nil
}
type policyQualifier struct {
type policyInformation struct {
PolicyIdentifier asn1.ObjectIdentifier
Qualifiers []interface{}
CPSPolicyQualifiers []cpsPolicyQualifier `asn1:"omitempty"`
// User Notice policy qualifiers have a slightly different ASN.1 structure
// from that used for CPS policy qualifiers.
UserNoticePolicyQualifiers []userNoticePolicyQualifier `asn1:"omitempty"`
}
type cpsPolicyQualifier struct {
PolicyQualifierID asn1.ObjectIdentifier
Qualifier string `asn1:"tag:optional,ia5"`
}
type policyInformation struct {
PolicyIdentifier asn1.ObjectIdentifier
PolicyQualifiers []policyQualifier `asn1:"omitempty"`
type userNotice struct {
ExplicitText string `asn1:"tag:optional,utf8"`
}
type userNoticePolicyQualifier struct {
PolicyQualifierID asn1.ObjectIdentifier
Qualifier userNotice
}
var (
@ -382,26 +387,25 @@ func addPolicies(template *x509.Certificate, policies []config.CertificatePolicy
// The PolicyIdentifier is an OID assigned to a given issuer.
PolicyIdentifier: asn1.ObjectIdentifier(policy.ID),
}
switch policy.Type {
case "id-qt-unotice":
pi.PolicyQualifiers = []policyQualifier{
policyQualifier{
PolicyQualifierID: iDQTUserNotice,
Qualifier: policy.Qualifier,
},
for _, qualifier := range policy.Qualifiers {
switch qualifier.Type {
case "id-qt-unotice":
pi.Qualifiers = append(pi.Qualifiers,
userNoticePolicyQualifier{
PolicyQualifierID: iDQTUserNotice,
Qualifier: userNotice{
ExplicitText: qualifier.Value,
},
})
case "id-qt-cps":
pi.Qualifiers = append(pi.Qualifiers,
cpsPolicyQualifier{
PolicyQualifierID: iDQTCertificationPracticeStatement,
Qualifier: qualifier.Value,
})
default:
return errors.New("Invalid qualifier type in Policies " + qualifier.Type)
}
case "id-qt-cps":
pi.PolicyQualifiers = []policyQualifier{
policyQualifier{
PolicyQualifierID: iDQTCertificationPracticeStatement,
Qualifier: policy.Qualifier,
},
}
case "":
// Empty qualifier type is fine: Include this Certificate Policy, but
// don't include a Policy Qualifier.
default:
return errors.New("Invalid qualifier type in Policies " + policy.Type)
}
asn1PolicyList = append(asn1PolicyList, pi)
}

View File

@ -1,7 +1,15 @@
package signer
import (
"bytes"
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"fmt"
"reflect"
"testing"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
)
func TestSplitHosts(t *testing.T) {
@ -23,3 +31,63 @@ func TestSplitHosts(t *testing.T) {
t.Fatal("SplitHost fails to split multiple domains")
}
}
func TestAddPolicies(t *testing.T) {
var cert x509.Certificate
addPolicies(&cert, []config.CertificatePolicy{
config.CertificatePolicy{
ID: config.OID{1, 2, 3, 4},
},
})
if len(cert.ExtraExtensions) != 1 {
t.Fatal("No extension added")
}
ext := cert.ExtraExtensions[0]
if !reflect.DeepEqual(ext.Id, asn1.ObjectIdentifier{2, 5, 29, 32}) {
t.Fatal(fmt.Sprintf("Wrong OID for policy qualifier %v", ext.Id))
}
if ext.Critical {
t.Fatal("Policy qualifier marked critical")
}
expectedBytes, _ := hex.DecodeString("3009300706032a03043000")
if !bytes.Equal(ext.Value, expectedBytes) {
t.Fatal(fmt.Sprintf("Value didn't match expected bytes: %s vs %s",
hex.EncodeToString(ext.Value), hex.EncodeToString(expectedBytes)))
}
}
func TestAddPoliciesWithQualifiers(t *testing.T) {
var cert x509.Certificate
addPolicies(&cert, []config.CertificatePolicy{
config.CertificatePolicy{
ID: config.OID{1, 2, 3, 4},
Qualifiers: []config.CertificatePolicyQualifier{
config.CertificatePolicyQualifier{
Type: "id-qt-cps",
Value: "http://example.com/cps",
},
config.CertificatePolicyQualifier{
Type: "id-qt-unotice",
Value: "Do What Thou Wilt",
},
},
},
})
if len(cert.ExtraExtensions) != 1 {
t.Fatal("No extension added")
}
ext := cert.ExtraExtensions[0]
if !reflect.DeepEqual(ext.Id, asn1.ObjectIdentifier{2, 5, 29, 32}) {
t.Fatal(fmt.Sprintf("Wrong OID for policy qualifier %v", ext.Id))
}
if ext.Critical {
t.Fatal("Policy qualifier marked critical")
}
expectedBytes, _ := hex.DecodeString("304e304c06032a03043045302206082b060105050702011616687474703a2f2f6578616d706c652e636f6d2f637073301f06082b0601050507020230130c11446f20576861742054686f752057696c74")
if !bytes.Equal(ext.Value, expectedBytes) {
t.Fatal(fmt.Sprintf("Value didn't match expected bytes: %s vs %s",
hex.EncodeToString(ext.Value), hex.EncodeToString(expectedBytes)))
}
}

View File

@ -1,21 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Damian Gryski <damian@gryski.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The MIT License (MIT)
Copyright (c) 2015 Damian Gryski <damian@gryski.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -13,15 +13,21 @@ go:
- 1.4
- tip
before_script:
- export PATH=$HOME/.local/bin:$PATH
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover || true
- go get code.google.com/p/go.tools/cmd/cover || true
- pip install cram --user `whoami`
script:
- go test . -v -covermode=count -coverprofile=profile.cov
- go test ./cipher -v -covermode=count -coverprofile=cipher/profile.cov
- cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t
- cd ..
after_success:
- tail -n+2 cipher/profile.cov >> profile.cov

View File

@ -17,10 +17,10 @@ US maintained blocked list.
## Overview
The implementation follows the
[JSON Web Encryption](http://www.ietf.org/id/draft-ietf-jose-json-web-encryption-40.txt)
standard (as of version 40) and
[JSON Web Signature](http://www.ietf.org/id/draft-ietf-jose-json-web-signature-41.txt)
standard (as of version 41). Tables of supported algorithms are shown below.
[JSON Web Encryption](http://dx.doi.org/10.17487/RFC7516)
standard (RFC 7516) and
[JSON Web Signature](http://dx.doi.org/10.17487/RFC7515)
standard (RFC 7515). Tables of supported algorithms are shown below.
The library supports both the compact and full serialization formats, and has
optional support for multiple recipients. It also comes with a small
command-line utility (`jose-util`) for encrypting/decrypting JWE messages in a
@ -30,7 +30,7 @@ shell.
See below for a table of supported algorithms. Algorithm identifiers match
the names in the
[JSON Web Algorithms](http://www.ietf.org/id/draft-ietf-jose-json-web-algorithms-40.txt)
[JSON Web Algorithms](http://dx.doi.org/10.17487/RFC7518)
standard where possible. The
[Godoc reference](https://godoc.org/github.com/square/go-jose#pkg-constants)
has a list of constants.

View File

@ -252,7 +252,7 @@ func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm
}
return Signature{
signature: out,
Signature: out,
protected: &rawHeader{},
}, nil
}
@ -454,7 +454,7 @@ func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm)
out := append(rBytesPadded, sBytesPadded...)
return Signature{
signature: out,
Signature: out,
protected: &rawHeader{},
}, nil
}

View File

@ -0,0 +1,189 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"bytes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/binary"
"errors"
"hash"
)
const (
nonceBytes = 16
)
// NewCBCHMAC instantiates a new AEAD based on CBC+HMAC.
func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) {
keySize := len(key) / 2
integrityKey := key[:keySize]
encryptionKey := key[keySize:]
blockCipher, err := newBlockCipher(encryptionKey)
if err != nil {
return nil, err
}
var hash func() hash.Hash
switch keySize {
case 16:
hash = sha256.New
case 24:
hash = sha512.New384
case 32:
hash = sha512.New
}
return &cbcAEAD{
hash: hash,
blockCipher: blockCipher,
authtagBytes: keySize,
integrityKey: integrityKey,
}, nil
}
// An AEAD based on CBC+HMAC
type cbcAEAD struct {
hash func() hash.Hash
authtagBytes int
integrityKey []byte
blockCipher cipher.Block
}
func (ctx *cbcAEAD) NonceSize() int {
return nonceBytes
}
func (ctx *cbcAEAD) Overhead() int {
// Maximum overhead is block size (for padding) plus auth tag length, where
// the length of the auth tag is equivalent to the key size.
return ctx.blockCipher.BlockSize() + ctx.authtagBytes
}
// Seal encrypts and authenticates the plaintext.
func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte {
// Output buffer -- must take care not to mangle plaintext input.
ciphertext := make([]byte, len(plaintext)+ctx.Overhead())[:len(plaintext)]
copy(ciphertext, plaintext)
ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize())
cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce)
cbc.CryptBlocks(ciphertext, ciphertext)
authtag := ctx.computeAuthTag(data, nonce, ciphertext)
ret, out := resize(dst, len(dst)+len(ciphertext)+len(authtag))
copy(out, ciphertext)
copy(out[len(ciphertext):], authtag)
return ret
}
// Open decrypts and authenticates the ciphertext.
func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
if len(ciphertext) < ctx.authtagBytes {
return nil, errors.New("square/go-jose: invalid ciphertext (too short)")
}
offset := len(ciphertext) - ctx.authtagBytes
expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset])
match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:])
if match != 1 {
return nil, errors.New("square/go-jose: invalid ciphertext (auth tag mismatch)")
}
cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce)
buffer := []byte(ciphertext[:offset])
cbc.CryptBlocks(buffer, buffer)
// Remove padding
plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize())
if err != nil {
return nil, err
}
ret, out := resize(dst, len(dst)+len(plaintext))
copy(out, plaintext)
return ret, nil
}
// Compute an authentication tag
func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte {
buffer := make([]byte, len(aad)+len(nonce)+len(ciphertext)+8)
n := 0
n += copy(buffer, aad)
n += copy(buffer[n:], nonce)
n += copy(buffer[n:], ciphertext)
binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad)*8))
// According to documentation, Write() on hash.Hash never fails.
hmac := hmac.New(ctx.hash, ctx.integrityKey)
_, _ = hmac.Write(buffer)
return hmac.Sum(nil)[:ctx.authtagBytes]
}
// resize ensures the the given slice has a capacity of at least n bytes.
// If the capacity of the slice is less than n, a new slice is allocated
// and the existing data will be copied.
func resize(in []byte, n int) (head, tail []byte) {
if cap(in) >= n {
head = in[:n]
} else {
head = make([]byte, n)
copy(head, in)
}
tail = head[len(in):]
return
}
// Apply padding
func padBuffer(buffer []byte, blockSize int) []byte {
missing := blockSize - (len(buffer) % blockSize)
ret, out := resize(buffer, len(buffer)+missing)
padding := bytes.Repeat([]byte{byte(missing)}, missing)
copy(out, padding)
return ret
}
// Remove padding
func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) {
if len(buffer)%blockSize != 0 {
return nil, errors.New("square/go-jose: invalid padding")
}
last := buffer[len(buffer)-1]
count := int(last)
if count > blockSize || count > len(buffer) {
return nil, errors.New("square/go-jose: invalid padding")
}
padding := bytes.Repeat([]byte{last}, count)
if !bytes.HasSuffix(buffer, padding) {
return nil, errors.New("square/go-jose: invalid padding")
}
return buffer[:len(buffer)-count], nil
}

View File

@ -0,0 +1,458 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"bytes"
"crypto/aes"
"crypto/rand"
"io"
"testing"
)
func TestInvalidInputs(t *testing.T) {
key := []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
}
nonce := []byte{
92, 80, 104, 49, 133, 25, 161, 215, 173, 101, 219, 211, 136, 91, 210, 145}
aead, _ := NewCBCHMAC(key, aes.NewCipher)
ciphertext := aead.Seal(nil, nonce, []byte("plaintext"), []byte("aad"))
// Changed AAD, must fail
_, err := aead.Open(nil, nonce, ciphertext, []byte("INVALID"))
if err == nil {
t.Error("must detect invalid aad")
}
// Empty ciphertext, must fail
_, err = aead.Open(nil, nonce, []byte{}, []byte("aad"))
if err == nil {
t.Error("must detect invalid/empty ciphertext")
}
// Corrupt ciphertext, must fail
corrupt := make([]byte, len(ciphertext))
copy(corrupt, ciphertext)
corrupt[0] ^= 0xFF
_, err = aead.Open(nil, nonce, corrupt, []byte("aad"))
if err == nil {
t.Error("must detect corrupt ciphertext")
}
// Corrupt authtag, must fail
copy(corrupt, ciphertext)
corrupt[len(ciphertext)-1] ^= 0xFF
_, err = aead.Open(nil, nonce, corrupt, []byte("aad"))
if err == nil {
t.Error("must detect corrupt authtag")
}
// Truncated data, must fail
_, err = aead.Open(nil, nonce, ciphertext[:10], []byte("aad"))
if err == nil {
t.Error("must detect corrupt authtag")
}
}
func TestVectorsAESCBC128(t *testing.T) {
// Source: http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-29#appendix-A.2
plaintext := []byte{
76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32,
112, 114, 111, 115, 112, 101, 114, 46}
aad := []byte{
101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69,
120, 88, 122, 85, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105,
74, 66, 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85,
50, 73, 110, 48}
expectedCiphertext := []byte{
40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6,
75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143,
112, 56, 102}
expectedAuthtag := []byte{
246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100,
191}
key := []byte{
4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206,
107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207}
nonce := []byte{
3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101}
enc, err := NewCBCHMAC(key, aes.NewCipher)
out := enc.Seal(nil, nonce, plaintext, aad)
if err != nil {
t.Error("Unable to encrypt:", err)
return
}
if bytes.Compare(out[:len(out)-16], expectedCiphertext) != 0 {
t.Error("Ciphertext did not match")
}
if bytes.Compare(out[len(out)-16:], expectedAuthtag) != 0 {
t.Error("Auth tag did not match")
}
}
func TestVectorsAESCBC256(t *testing.T) {
// Source: https://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5.4
plaintext := []byte{
0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65}
aad := []byte{
0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73}
expectedCiphertext := []byte{
0x4a, 0xff, 0xaa, 0xad, 0xb7, 0x8c, 0x31, 0xc5, 0xda, 0x4b, 0x1b, 0x59, 0x0d, 0x10, 0xff, 0xbd,
0x3d, 0xd8, 0xd5, 0xd3, 0x02, 0x42, 0x35, 0x26, 0x91, 0x2d, 0xa0, 0x37, 0xec, 0xbc, 0xc7, 0xbd,
0x82, 0x2c, 0x30, 0x1d, 0xd6, 0x7c, 0x37, 0x3b, 0xcc, 0xb5, 0x84, 0xad, 0x3e, 0x92, 0x79, 0xc2,
0xe6, 0xd1, 0x2a, 0x13, 0x74, 0xb7, 0x7f, 0x07, 0x75, 0x53, 0xdf, 0x82, 0x94, 0x10, 0x44, 0x6b,
0x36, 0xeb, 0xd9, 0x70, 0x66, 0x29, 0x6a, 0xe6, 0x42, 0x7e, 0xa7, 0x5c, 0x2e, 0x08, 0x46, 0xa1,
0x1a, 0x09, 0xcc, 0xf5, 0x37, 0x0d, 0xc8, 0x0b, 0xfe, 0xcb, 0xad, 0x28, 0xc7, 0x3f, 0x09, 0xb3,
0xa3, 0xb7, 0x5e, 0x66, 0x2a, 0x25, 0x94, 0x41, 0x0a, 0xe4, 0x96, 0xb2, 0xe2, 0xe6, 0x60, 0x9e,
0x31, 0xe6, 0xe0, 0x2c, 0xc8, 0x37, 0xf0, 0x53, 0xd2, 0x1f, 0x37, 0xff, 0x4f, 0x51, 0x95, 0x0b,
0xbe, 0x26, 0x38, 0xd0, 0x9d, 0xd7, 0xa4, 0x93, 0x09, 0x30, 0x80, 0x6d, 0x07, 0x03, 0xb1, 0xf6}
expectedAuthtag := []byte{
0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf,
0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5}
key := []byte{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f}
nonce := []byte{
0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04}
enc, err := NewCBCHMAC(key, aes.NewCipher)
out := enc.Seal(nil, nonce, plaintext, aad)
if err != nil {
t.Error("Unable to encrypt:", err)
return
}
if bytes.Compare(out[:len(out)-32], expectedCiphertext) != 0 {
t.Error("Ciphertext did not match, got", out[:len(out)-32], "wanted", expectedCiphertext)
}
if bytes.Compare(out[len(out)-32:], expectedAuthtag) != 0 {
t.Error("Auth tag did not match, got", out[len(out)-32:], "wanted", expectedAuthtag)
}
}
func TestAESCBCRoundtrip(t *testing.T) {
key128 := []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
key192 := []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7}
key256 := []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
nonce := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
RunRoundtrip(t, key128, nonce)
RunRoundtrip(t, key192, nonce)
RunRoundtrip(t, key256, nonce)
}
func RunRoundtrip(t *testing.T, key, nonce []byte) {
aead, err := NewCBCHMAC(key, aes.NewCipher)
if err != nil {
panic(err)
}
if aead.NonceSize() != len(nonce) {
panic("invalid nonce")
}
// Test pre-existing data in dst buffer
dst := []byte{15, 15, 15, 15}
plaintext := []byte{0, 0, 0, 0}
aad := []byte{4, 3, 2, 1}
result := aead.Seal(dst, nonce, plaintext, aad)
if bytes.Compare(dst, result[:4]) != 0 {
t.Error("Existing data in dst not preserved")
}
// Test pre-existing (empty) dst buffer with sufficient capacity
dst = make([]byte, 256)[:0]
result, err = aead.Open(dst, nonce, result[4:], aad)
if err != nil {
panic(err)
}
if bytes.Compare(result, plaintext) != 0 {
t.Error("Plaintext does not match output")
}
}
func TestAESCBCOverhead(t *testing.T) {
aead, err := NewCBCHMAC(make([]byte, 32), aes.NewCipher)
if err != nil {
panic(err)
}
if aead.Overhead() != 32 {
t.Error("CBC-HMAC reports incorrect overhead value")
}
}
func TestPadding(t *testing.T) {
for i := 0; i < 256; i++ {
slice := make([]byte, i)
padded := padBuffer(slice, 16)
if len(padded)%16 != 0 {
t.Error("failed to pad slice properly", i)
return
}
unpadded, err := unpadBuffer(padded, 16)
if err != nil || len(unpadded) != i {
t.Error("failed to unpad slice properly", i)
return
}
}
}
func TestInvalidKey(t *testing.T) {
key := make([]byte, 30)
_, err := NewCBCHMAC(key, aes.NewCipher)
if err == nil {
t.Error("should not be able to instantiate CBC-HMAC with invalid key")
}
}
func TestInvalidCiphertext(t *testing.T) {
key := make([]byte, 32)
nonce := make([]byte, 16)
data := make([]byte, 32)
io.ReadFull(rand.Reader, key)
io.ReadFull(rand.Reader, nonce)
aead, err := NewCBCHMAC(key, aes.NewCipher)
if err != nil {
panic(err)
}
ctx := aead.(*cbcAEAD)
ct := aead.Seal(nil, nonce, data, nil)
// Mutated ciphertext, but with correct auth tag
ct[len(ct)-ctx.authtagBytes-1] ^= 0xFF
tag := ctx.computeAuthTag(nil, nonce, ct[:len(ct)-ctx.authtagBytes])
copy(ct[len(ct)-ctx.authtagBytes:], tag)
// Open should fail (b/c of invalid padding, even though tag matches)
_, err = aead.Open(nil, nonce, ct, nil)
if err == nil {
t.Error("open on mutated ciphertext should fail")
}
}
func TestInvalidPadding(t *testing.T) {
for i := 0; i < 256; i++ {
slice := make([]byte, i)
padded := padBuffer(slice, 16)
if len(padded)%16 != 0 {
t.Error("failed to pad slice properly", i)
return
}
paddingBytes := 16 - (i % 16)
// Mutate padding for testing
for j := 1; j <= paddingBytes; j++ {
mutated := make([]byte, len(padded))
copy(mutated, padded)
mutated[len(mutated)-j] ^= 0xFF
_, err := unpadBuffer(mutated, 16)
if err == nil {
t.Error("unpad on invalid padding should fail", i)
return
}
}
// Test truncated padding
_, err := unpadBuffer(padded[:len(padded)-1], 16)
if err == nil {
t.Error("unpad on truncated padding should fail", i)
return
}
}
}
func benchEncryptCBCHMAC(b *testing.B, keySize, chunkSize int) {
key := make([]byte, keySize*2)
nonce := make([]byte, 16)
io.ReadFull(rand.Reader, key)
io.ReadFull(rand.Reader, nonce)
chunk := make([]byte, chunkSize)
aead, err := NewCBCHMAC(key, aes.NewCipher)
if err != nil {
panic(err)
}
b.SetBytes(int64(chunkSize))
b.ResetTimer()
for i := 0; i < b.N; i++ {
aead.Seal(nil, nonce, chunk, nil)
}
}
func benchDecryptCBCHMAC(b *testing.B, keySize, chunkSize int) {
key := make([]byte, keySize*2)
nonce := make([]byte, 16)
io.ReadFull(rand.Reader, key)
io.ReadFull(rand.Reader, nonce)
chunk := make([]byte, chunkSize)
aead, err := NewCBCHMAC(key, aes.NewCipher)
if err != nil {
panic(err)
}
out := aead.Seal(nil, nonce, chunk, nil)
b.SetBytes(int64(chunkSize))
b.ResetTimer()
for i := 0; i < b.N; i++ {
aead.Open(nil, nonce, out, nil)
}
}
func BenchmarkEncryptAES128_CBCHMAC_1k(b *testing.B) {
benchEncryptCBCHMAC(b, 16, 1024)
}
func BenchmarkEncryptAES128_CBCHMAC_64k(b *testing.B) {
benchEncryptCBCHMAC(b, 16, 65536)
}
func BenchmarkEncryptAES128_CBCHMAC_1MB(b *testing.B) {
benchEncryptCBCHMAC(b, 16, 1048576)
}
func BenchmarkEncryptAES128_CBCHMAC_64MB(b *testing.B) {
benchEncryptCBCHMAC(b, 16, 67108864)
}
func BenchmarkDecryptAES128_CBCHMAC_1k(b *testing.B) {
benchDecryptCBCHMAC(b, 16, 1024)
}
func BenchmarkDecryptAES128_CBCHMAC_64k(b *testing.B) {
benchDecryptCBCHMAC(b, 16, 65536)
}
func BenchmarkDecryptAES128_CBCHMAC_1MB(b *testing.B) {
benchDecryptCBCHMAC(b, 16, 1048576)
}
func BenchmarkDecryptAES128_CBCHMAC_64MB(b *testing.B) {
benchDecryptCBCHMAC(b, 16, 67108864)
}
func BenchmarkEncryptAES192_CBCHMAC_64k(b *testing.B) {
benchEncryptCBCHMAC(b, 24, 65536)
}
func BenchmarkEncryptAES192_CBCHMAC_1MB(b *testing.B) {
benchEncryptCBCHMAC(b, 24, 1048576)
}
func BenchmarkEncryptAES192_CBCHMAC_64MB(b *testing.B) {
benchEncryptCBCHMAC(b, 24, 67108864)
}
func BenchmarkDecryptAES192_CBCHMAC_1k(b *testing.B) {
benchDecryptCBCHMAC(b, 24, 1024)
}
func BenchmarkDecryptAES192_CBCHMAC_64k(b *testing.B) {
benchDecryptCBCHMAC(b, 24, 65536)
}
func BenchmarkDecryptAES192_CBCHMAC_1MB(b *testing.B) {
benchDecryptCBCHMAC(b, 24, 1048576)
}
func BenchmarkDecryptAES192_CBCHMAC_64MB(b *testing.B) {
benchDecryptCBCHMAC(b, 24, 67108864)
}
func BenchmarkEncryptAES256_CBCHMAC_64k(b *testing.B) {
benchEncryptCBCHMAC(b, 32, 65536)
}
func BenchmarkEncryptAES256_CBCHMAC_1MB(b *testing.B) {
benchEncryptCBCHMAC(b, 32, 1048576)
}
func BenchmarkEncryptAES256_CBCHMAC_64MB(b *testing.B) {
benchEncryptCBCHMAC(b, 32, 67108864)
}
func BenchmarkDecryptAES256_CBCHMAC_1k(b *testing.B) {
benchDecryptCBCHMAC(b, 32, 1032)
}
func BenchmarkDecryptAES256_CBCHMAC_64k(b *testing.B) {
benchDecryptCBCHMAC(b, 32, 65536)
}
func BenchmarkDecryptAES256_CBCHMAC_1MB(b *testing.B) {
benchDecryptCBCHMAC(b, 32, 1048576)
}
func BenchmarkDecryptAES256_CBCHMAC_64MB(b *testing.B) {
benchDecryptCBCHMAC(b, 32, 67108864)
}

View File

@ -0,0 +1,75 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"crypto"
"encoding/binary"
"hash"
"io"
)
type concatKDF struct {
z, info []byte
i uint32
cache []byte
hasher hash.Hash
}
// NewConcatKDF builds a KDF reader based on the given inputs.
func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader {
buffer := make([]byte, len(algID)+len(ptyUInfo)+len(ptyVInfo)+len(supPubInfo)+len(supPrivInfo))
n := 0
n += copy(buffer, algID)
n += copy(buffer[n:], ptyUInfo)
n += copy(buffer[n:], ptyVInfo)
n += copy(buffer[n:], supPubInfo)
copy(buffer[n:], supPrivInfo)
hasher := hash.New()
return &concatKDF{
z: z,
info: buffer,
hasher: hasher,
cache: []byte{},
i: 1,
}
}
func (ctx *concatKDF) Read(out []byte) (int, error) {
copied := copy(out, ctx.cache)
ctx.cache = ctx.cache[copied:]
for copied < len(out) {
ctx.hasher.Reset()
// Write on a hash.Hash never fails
_ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i)
_, _ = ctx.hasher.Write(ctx.z)
_, _ = ctx.hasher.Write(ctx.info)
hash := ctx.hasher.Sum(nil)
chunkCopied := copy(out[copied:], hash)
copied += chunkCopied
ctx.cache = hash[chunkCopied:]
ctx.i++
}
return copied, nil
}

View File

@ -0,0 +1,148 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"bytes"
"crypto"
"testing"
)
// Taken from: https://tools.ietf.org/id/draft-ietf-jose-json-web-algorithms-38.txt
func TestVectorConcatKDF(t *testing.T) {
z := []byte{
158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132,
38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121,
140, 254, 144, 196}
algID := []byte{0, 0, 0, 7, 65, 49, 50, 56, 71, 67, 77}
ptyUInfo := []byte{0, 0, 0, 5, 65, 108, 105, 99, 101}
ptyVInfo := []byte{0, 0, 0, 3, 66, 111, 98}
supPubInfo := []byte{0, 0, 0, 128}
supPrivInfo := []byte{}
expected := []byte{
86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26}
ckdf := NewConcatKDF(crypto.SHA256, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo)
out0 := make([]byte, 9)
out1 := make([]byte, 7)
read0, err := ckdf.Read(out0)
if err != nil {
t.Error("error when reading from concat kdf reader", err)
return
}
read1, err := ckdf.Read(out1)
if err != nil {
t.Error("error when reading from concat kdf reader", err)
return
}
if read0+read1 != len(out0)+len(out1) {
t.Error("did not receive enough bytes from concat kdf reader")
return
}
out := []byte{}
out = append(out, out0...)
out = append(out, out1...)
if bytes.Compare(out, expected) != 0 {
t.Error("did not receive expected output from concat kdf reader")
return
}
}
func TestCache(t *testing.T) {
z := []byte{
158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132,
38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121,
140, 254, 144, 196}
algID := []byte{1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4}
ptyUInfo := []byte{1, 2, 3, 4}
ptyVInfo := []byte{4, 3, 2, 1}
supPubInfo := []byte{}
supPrivInfo := []byte{}
outputs := [][]byte{}
// Read the same amount of data in different chunk sizes
for i := 10; i <= 100; i++ {
out := make([]byte, 1024)
reader := NewConcatKDF(crypto.SHA256, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo)
for j := 0; j < 1024/i; j++ {
_, _ = reader.Read(out[j*i:])
}
outputs = append(outputs, out)
}
for i := range outputs {
if bytes.Compare(outputs[i], outputs[i%len(outputs)]) != 0 {
t.Error("not all outputs from KDF matched")
}
}
}
func benchmarkKDF(b *testing.B, total int) {
z := []byte{
158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132,
38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121,
140, 254, 144, 196}
algID := []byte{1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4}
ptyUInfo := []byte{1, 2, 3, 4}
ptyVInfo := []byte{4, 3, 2, 1}
supPubInfo := []byte{}
supPrivInfo := []byte{}
out := make([]byte, total)
reader := NewConcatKDF(crypto.SHA256, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo)
b.ResetTimer()
b.SetBytes(int64(total))
for i := 0; i < b.N; i++ {
_, _ = reader.Read(out)
}
}
func BenchmarkConcatKDF_1k(b *testing.B) {
benchmarkKDF(b, 1024)
}
func BenchmarkConcatKDF_64k(b *testing.B) {
benchmarkKDF(b, 65536)
}
func BenchmarkConcatKDF_1MB(b *testing.B) {
benchmarkKDF(b, 1048576)
}
func BenchmarkConcatKDF_64MB(b *testing.B) {
benchmarkKDF(b, 67108864)
}

View File

@ -0,0 +1,51 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"crypto"
"crypto/ecdsa"
"encoding/binary"
)
// DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA.
func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte {
// algId, partyUInfo, partyVInfo inputs must be prefixed with the length
algID := lengthPrefixed([]byte(alg))
ptyUInfo := lengthPrefixed(apuData)
ptyVInfo := lengthPrefixed(apvData)
// suppPubInfo is the encoded length of the output size in bits
supPubInfo := make([]byte, 4)
binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8)
z, _ := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes())
reader := NewConcatKDF(crypto.SHA256, z.Bytes(), algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{})
key := make([]byte, size)
// Read on the KDF will never fail
_, _ = reader.Read(key)
return key
}
func lengthPrefixed(data []byte) []byte {
out := make([]byte, len(data)+4)
binary.BigEndian.PutUint32(out, uint32(len(data)))
copy(out[4:], data)
return out
}

View File

@ -0,0 +1,98 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"encoding/base64"
"math/big"
"testing"
)
// Example keys from JWA, Appendix C
var aliceKey = &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: elliptic.P256(),
X: fromBase64Int("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0="),
Y: fromBase64Int("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps="),
},
D: fromBase64Int("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo="),
}
var bobKey = &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: elliptic.P256(),
X: fromBase64Int("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ="),
Y: fromBase64Int("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck="),
},
D: fromBase64Int("VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw="),
}
// Build big int from base64-encoded string. Strips whitespace (for testing).
func fromBase64Int(data string) *big.Int {
val, err := base64.URLEncoding.DecodeString(data)
if err != nil {
panic("Invalid test data")
}
return new(big.Int).SetBytes(val)
}
func TestVectorECDHES(t *testing.T) {
apuData := []byte("Alice")
apvData := []byte("Bob")
expected := []byte{
86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26}
output := DeriveECDHES("A128GCM", apuData, apvData, bobKey, &aliceKey.PublicKey, 16)
if bytes.Compare(output, expected) != 0 {
t.Error("output did not match what we expect, got", output, "wanted", expected)
}
}
func BenchmarkECDHES_128(b *testing.B) {
apuData := []byte("APU")
apvData := []byte("APV")
b.ResetTimer()
for i := 0; i < b.N; i++ {
DeriveECDHES("ID", apuData, apvData, bobKey, &aliceKey.PublicKey, 16)
}
}
func BenchmarkECDHES_192(b *testing.B) {
apuData := []byte("APU")
apvData := []byte("APV")
b.ResetTimer()
for i := 0; i < b.N; i++ {
DeriveECDHES("ID", apuData, apvData, bobKey, &aliceKey.PublicKey, 24)
}
}
func BenchmarkECDHES_256(b *testing.B) {
apuData := []byte("APU")
apvData := []byte("APV")
b.ResetTimer()
for i := 0; i < b.N; i++ {
DeriveECDHES("ID", apuData, apvData, bobKey, &aliceKey.PublicKey, 32)
}
}

View File

@ -0,0 +1,109 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"crypto/cipher"
"crypto/subtle"
"encoding/binary"
"errors"
)
var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6}
// KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher.
func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) {
if len(cek)%8 != 0 {
return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks")
}
n := len(cek) / 8
r := make([][]byte, n)
for i := range r {
r[i] = make([]byte, 8)
copy(r[i], cek[i*8:])
}
buffer := make([]byte, 16)
tBytes := make([]byte, 8)
copy(buffer, defaultIV)
for t := 0; t < 6*n; t++ {
copy(buffer[8:], r[t%n])
block.Encrypt(buffer, buffer)
binary.BigEndian.PutUint64(tBytes, uint64(t+1))
for i := 0; i < 8; i++ {
buffer[i] = buffer[i] ^ tBytes[i]
}
copy(r[t%n], buffer[8:])
}
out := make([]byte, (n+1)*8)
copy(out, buffer[:8])
for i := range r {
copy(out[(i+1)*8:], r[i])
}
return out, nil
}
// KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher.
func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) {
if len(ciphertext)%8 != 0 {
return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks")
}
n := (len(ciphertext) / 8) - 1
r := make([][]byte, n)
for i := range r {
r[i] = make([]byte, 8)
copy(r[i], ciphertext[(i+1)*8:])
}
buffer := make([]byte, 16)
tBytes := make([]byte, 8)
copy(buffer[:8], ciphertext[:8])
for t := 6*n - 1; t >= 0; t-- {
binary.BigEndian.PutUint64(tBytes, uint64(t+1))
for i := 0; i < 8; i++ {
buffer[i] = buffer[i] ^ tBytes[i]
}
copy(buffer[8:], r[t%n])
block.Decrypt(buffer, buffer)
copy(r[t%n], buffer[8:])
}
if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 {
return nil, errors.New("square/go-jose: failed to unwrap key")
}
out := make([]byte, n*8)
for i := range r {
copy(out[i*8:], r[i])
}
return out, nil
}

View File

@ -0,0 +1,133 @@
/*-
* Copyright 2014 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package josecipher
import (
"bytes"
"crypto/aes"
"encoding/hex"
"testing"
)
func TestAesKeyWrap(t *testing.T) {
// Test vectors from: http://csrc.nist.gov/groups/ST/toolkit/documents/kms/key-wrap.pdf
kek0, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F")
cek0, _ := hex.DecodeString("00112233445566778899AABBCCDDEEFF")
expected0, _ := hex.DecodeString("1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5")
kek1, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F1011121314151617")
cek1, _ := hex.DecodeString("00112233445566778899AABBCCDDEEFF")
expected1, _ := hex.DecodeString("96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D")
kek2, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F")
cek2, _ := hex.DecodeString("00112233445566778899AABBCCDDEEFF0001020304050607")
expected2, _ := hex.DecodeString("A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1")
block0, _ := aes.NewCipher(kek0)
block1, _ := aes.NewCipher(kek1)
block2, _ := aes.NewCipher(kek2)
out0, _ := KeyWrap(block0, cek0)
out1, _ := KeyWrap(block1, cek1)
out2, _ := KeyWrap(block2, cek2)
if bytes.Compare(out0, expected0) != 0 {
t.Error("output 0 not as expected, got", out0, "wanted", expected0)
}
if bytes.Compare(out1, expected1) != 0 {
t.Error("output 1 not as expected, got", out1, "wanted", expected1)
}
if bytes.Compare(out2, expected2) != 0 {
t.Error("output 2 not as expected, got", out2, "wanted", expected2)
}
unwrap0, _ := KeyUnwrap(block0, out0)
unwrap1, _ := KeyUnwrap(block1, out1)
unwrap2, _ := KeyUnwrap(block2, out2)
if bytes.Compare(unwrap0, cek0) != 0 {
t.Error("key unwrap did not return original input, got", unwrap0, "wanted", cek0)
}
if bytes.Compare(unwrap1, cek1) != 0 {
t.Error("key unwrap did not return original input, got", unwrap1, "wanted", cek1)
}
if bytes.Compare(unwrap2, cek2) != 0 {
t.Error("key unwrap did not return original input, got", unwrap2, "wanted", cek2)
}
}
func TestAesKeyWrapInvalid(t *testing.T) {
kek, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F")
// Invalid unwrap input (bit flipped)
input0, _ := hex.DecodeString("1EA68C1A8112B447AEF34BD8FB5A7B828D3E862371D2CFE5")
block, _ := aes.NewCipher(kek)
_, err := KeyUnwrap(block, input0)
if err == nil {
t.Error("key unwrap failed to detect invalid input")
}
// Invalid unwrap input (truncated)
input1, _ := hex.DecodeString("1EA68C1A8112B447AEF34BD8FB5A7B828D3E862371D2CF")
_, err = KeyUnwrap(block, input1)
if err == nil {
t.Error("key unwrap failed to detect truncated input")
}
// Invalid wrap input (not multiple of 8)
input2, _ := hex.DecodeString("0123456789ABCD")
_, err = KeyWrap(block, input2)
if err == nil {
t.Error("key wrap accepted invalid input")
}
}
func BenchmarkAesKeyWrap(b *testing.B) {
kek, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F")
key, _ := hex.DecodeString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
block, _ := aes.NewCipher(kek)
b.ResetTimer()
for i := 0; i < b.N; i++ {
KeyWrap(block, key)
}
}
func BenchmarkAesKeyUnwrap(b *testing.B) {
kek, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F")
input, _ := hex.DecodeString("1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5")
block, _ := aes.NewCipher(kek)
b.ResetTimer()
for i := 0; i < b.N; i++ {
KeyUnwrap(block, input)
}
}

View File

@ -20,6 +20,7 @@ import (
"bytes"
"compress/flate"
"encoding/base64"
"encoding/binary"
"encoding/json"
"io"
"math/big"
@ -131,6 +132,12 @@ func newBuffer(data []byte) *byteBuffer {
}
}
func newBufferFromInt(num uint64) *byteBuffer {
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, num)
return newBuffer(bytes.TrimLeft(data, "\x00"))
}
func (b *byteBuffer) MarshalJSON() ([]byte, error) {
return json.Marshal(b.base64())
}

View File

@ -106,3 +106,15 @@ func TestInvalidCompression(t *testing.T) {
t.Error("should not accept invalid data")
}
}
func TestByteBufferTrim(t *testing.T) {
buf := newBufferFromInt(1)
if !bytes.Equal(buf.data, []byte{1}) {
t.Error("Byte buffer for integer '1' should contain [0x01]")
}
buf = newBufferFromInt(65537)
if !bytes.Equal(buf.data, []byte{1, 0, 1}) {
t.Error("Byte buffer for integer '65537' should contain [0x01, 0x00, 0x01]")
}
}

View File

@ -10,10 +10,10 @@ The utility includes the subcommands `encrypt`, `decrypt`, `sign`, `verify` and
`expand`. Examples for each command can be found below.
Algorithms are selected via the `--alg` and `--enc` flags, which influence the
`alg` and `enc` headers in JWE/JWS messages respectively. For JWE, `--alg`
specified the key managment algorithm (e.g. RSA-OAEP) and `--enc` specifies the
content encryption (e.g. A128GCM). For JWS, `--alg` specified the signature
algorithm (e.g. `PS256`).
`alg` and `enc` headers in respectively. For JWE, `--alg` specifies the key
managment algorithm (e.g. `RSA-OAEP`) and `--enc` specifies the content
encryption (e.g. `A128GCM`). For JWS, `--alg` specifies the signature algorithm
(e.g. `PS256`).
Input and output files can be specified via the `--in` and `--out` flags.
Either flag can be omitted, in which case `jose-util` uses stdin/stdout for

View File

@ -0,0 +1,88 @@
Set up test keys.
$ cat > rsa.pub <<EOF
> -----BEGIN PUBLIC KEY-----
> MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAslWybuiNYR7uOgKuvaBw
> qVk8saEutKhOAaW+3hWF65gJei+ZV8QFfYDxs9ZaRZlWAUMtncQPnw7ZQlXO9ogN
> 5cMcN50C6qMOOZzghK7danalhF5lUETC4Hk3Eisbi/PR3IfVyXaRmqL6X66MKj/J
> AKyD9NFIDVy52K8A198Jojnrw2+XXQW72U68fZtvlyl/BTBWQ9Re5JSTpEcVmpCR
> 8FrFc0RPMBm+G5dRs08vvhZNiTT2JACO5V+J5ZrgP3s5hnGFcQFZgDnXLInDUdoi
> 1MuCjaAU0ta8/08pHMijNix5kFofdPEB954MiZ9k4kQ5/utt02I9x2ssHqw71ojj
> vwIDAQAB
> -----END PUBLIC KEY-----
> EOF
$ cat > rsa.key <<EOF
> -----BEGIN RSA PRIVATE KEY-----
> MIIEogIBAAKCAQEAslWybuiNYR7uOgKuvaBwqVk8saEutKhOAaW+3hWF65gJei+Z
> V8QFfYDxs9ZaRZlWAUMtncQPnw7ZQlXO9ogN5cMcN50C6qMOOZzghK7danalhF5l
> UETC4Hk3Eisbi/PR3IfVyXaRmqL6X66MKj/JAKyD9NFIDVy52K8A198Jojnrw2+X
> XQW72U68fZtvlyl/BTBWQ9Re5JSTpEcVmpCR8FrFc0RPMBm+G5dRs08vvhZNiTT2
> JACO5V+J5ZrgP3s5hnGFcQFZgDnXLInDUdoi1MuCjaAU0ta8/08pHMijNix5kFof
> dPEB954MiZ9k4kQ5/utt02I9x2ssHqw71ojjvwIDAQABAoIBABrYDYDmXom1BzUS
> PE1s/ihvt1QhqA8nmn5i/aUeZkc9XofW7GUqq4zlwPxKEtKRL0IHY7Fw1s0hhhCX
> LA0uE7F3OiMg7lR1cOm5NI6kZ83jyCxxrRx1DUSO2nxQotfhPsDMbaDiyS4WxEts
> 0cp2SYJhdYd/jTH9uDfmt+DGwQN7Jixio1Dj3vwB7krDY+mdre4SFY7Gbk9VxkDg
> LgCLMoq52m+wYufP8CTgpKFpMb2/yJrbLhuJxYZrJ3qd/oYo/91k6v7xlBKEOkwD
> 2veGk9Dqi8YPNxaRktTEjnZb6ybhezat93+VVxq4Oem3wMwou1SfXrSUKtgM/p2H
> vfw/76ECgYEA2fNL9tC8u9M0wjA+kvvtDG96qO6O66Hksssy6RWInD+Iqk3MtHQt
> LeoCjvX+zERqwOb6SI6empk5pZ9E3/9vJ0dBqkxx3nqn4M/nRWnExGgngJsL959t
> f50cdxva8y1RjNhT4kCwTrupX/TP8lAG8SfG1Alo2VFR8iWd8hDQcTECgYEA0Xfj
> EgqAsVh4U0s3lFxKjOepEyp0G1Imty5J16SvcOEAD1Mrmz94aSSp0bYhXNVdbf7n
> Rk77htWC7SE29fGjOzZRS76wxj/SJHF+rktHB2Zt23k1jBeZ4uLMPMnGLY/BJ099
> 5DTGo0yU0rrPbyXosx+ukfQLAHFuggX4RNeM5+8CgYB7M1J/hGMLcUpjcs4MXCgV
> XXbiw2c6v1r9zmtK4odEe42PZ0cNwpY/XAZyNZAAe7Q0stxL44K4NWEmxC80x7lX
> ZKozz96WOpNnO16qGC3IMHAT/JD5Or+04WTT14Ue7UEp8qcIQDTpbJ9DxKk/eglS
> jH+SIHeKULOXw7fSu7p4IQKBgBnyVchIUMSnBtCagpn4DKwDjif3nEY+GNmb/D2g
> ArNiy5UaYk5qwEmV5ws5GkzbiSU07AUDh5ieHgetk5dHhUayZcOSLWeBRFCLVnvU
> i0nZYEZNb1qZGdDG8zGcdNXz9qMd76Qy/WAA/nZT+Zn1AiweAovFxQ8a/etRPf2Z
> DbU1AoGAHpCgP7B/4GTBe49H0AQueQHBn4RIkgqMy9xiMeR+U+U0vaY0TlfLhnX+
> 5PkNfkPXohXlfL7pxwZNYa6FZhCAubzvhKCdUASivkoGaIEk6g1VTVYS/eDVQ4CA
> slfl+elXtLq/l1kQ8C14jlHrQzSXx4PQvjDEnAmaHSJNz4mP9Fg=
> -----END RSA PRIVATE KEY-----
> EOF
$ cat > ec.pub <<EOF
> -----BEGIN PUBLIC KEY-----
> MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9yoUEAgxTd9svwe9oPqjhcP+f2jcdTL2
> Wq8Aw2v9ht1dBy00tFRPNrCxFCkvMcJFhSPoDUV5NL7zfh3/psiSNYziGPrWEJYf
> gmYihjSeoOf0ru1erpBrTflImPrMftCy
> -----END PUBLIC KEY-----
> EOF
$ cat > ec.key <<EOF
> -----BEGIN EC PRIVATE KEY-----
> MIGkAgEBBDDvoj/bM1HokUjYWO/IDFs26Jo0GIFtU3tMQQu7ZabKscDMK3dZA0mK
> v97ij7BBFbCgBwYFK4EEACKhZANiAAT3KhQQCDFN32y/B72g+qOFw/5/aNx1MvZa
> rwDDa/2G3V0HLTS0VE82sLEUKS8xwkWFI+gNRXk0vvN+Hf+myJI1jOIY+tYQlh+C
> ZiKGNJ6g5/Su7V6ukGtN+UiY+sx+0LI=
> -----END EC PRIVATE KEY-----
> EOF
Encrypt and then decrypt a test message (RSA).
$ echo "Lorem ipsum dolor sit amet" |
> jose-util encrypt --alg RSA-OAEP --enc A128GCM --key rsa.pub |
> jose-util decrypt --key rsa.key
Lorem ipsum dolor sit amet
Encrypt and then decrypt a test message (EC).
$ echo "Lorem ipsum dolor sit amet" |
> jose-util encrypt --alg ECDH-ES+A128KW --enc A128GCM --key ec.pub |
> jose-util decrypt --key ec.key
Lorem ipsum dolor sit amet
Sign and verify a test message (RSA).
$ echo "Lorem ipsum dolor sit amet" |
> jose-util sign --alg PS256 --key rsa.key |
> jose-util verify --key rsa.pub
Lorem ipsum dolor sit amet
Sign and verify a test message (EC).
$ echo "Lorem ipsum dolor sit amet" |
> jose-util sign --alg ES384 --key ec.key |
> jose-util verify --key ec.pub
Lorem ipsum dolor sit amet

View File

@ -22,7 +22,7 @@ import (
"os"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
"github.com/square/go-jose"
)
func main() {

View File

@ -216,7 +216,8 @@ func parseEncryptedCompact(input string) (*JsonWebEncryption, error) {
// CompactSerialize serializes an object using the compact serialization format.
func (obj JsonWebEncryption) CompactSerialize() (string, error) {
if len(obj.recipients) > 1 || obj.unprotected != nil || obj.recipients[0].header != nil {
if len(obj.recipients) != 1 || obj.unprotected != nil ||
obj.protected == nil || obj.recipients[0].header != nil {
return "", ErrNotSupported
}
@ -257,7 +258,9 @@ func (obj JsonWebEncryption) FullSerialize() string {
raw.EncryptedKey = newBuffer(obj.recipients[0].encryptedKey)
}
raw.Protected = newBuffer(mustSerializeJSON(obj.protected))
if obj.protected != nil {
raw.Protected = newBuffer(mustSerializeJSON(obj.protected))
}
return string(mustSerializeJSON(raw))
}

View File

@ -20,7 +20,6 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/binary"
"encoding/json"
"fmt"
"math/big"
@ -128,13 +127,10 @@ func (key rawJsonWebKey) rsaPublicKey() (*rsa.PublicKey, error) {
}
func fromRsaPublicKey(pub *rsa.PublicKey) *rawJsonWebKey {
e := make([]byte, 4)
binary.BigEndian.PutUint32(e, uint32(pub.E))
return &rawJsonWebKey{
Kty: "RSA",
N: newBuffer(pub.N.Bytes()),
E: newBuffer(e),
E: newBufferFromInt(uint64(pub.E)),
}
}

View File

@ -18,11 +18,11 @@ package jose
import (
"bytes"
"fmt"
"encoding/json"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/json"
"fmt"
"math/big"
"reflect"
"testing"
@ -119,7 +119,7 @@ func TestRoundtripEcPrivate(t *testing.T) {
}
}
func TestMarshalUnmarshal(t *testing.T) {
func TestMarshalUnmarshalJWK(t *testing.T) {
kid := "DEADBEEF"
for i, key := range []interface{}{ecTestKey256, ecTestKey384, ecTestKey521, rsaTestKey} {
@ -178,7 +178,7 @@ func TestMarshalNonPointer(t *testing.T) {
t.Error(fmt.Sprintf("Error marshalling JSON: %v", err))
return
}
expected := "{\"Key\":{\"kty\":\"RSA\",\"n\":\"vd7rZIoTLEe-z1_8G1FcXSw9CQFEJgV4g9V277sER7yx5Qjz_Pkf2YVth6wwwFJEmzc0hoKY-MMYFNwBE4hQHw\",\"e\":\"AAEAAQ\"}}"
expected := "{\"Key\":{\"kty\":\"RSA\",\"n\":\"vd7rZIoTLEe-z1_8G1FcXSw9CQFEJgV4g9V277sER7yx5Qjz_Pkf2YVth6wwwFJEmzc0hoKY-MMYFNwBE4hQHw\",\"e\":\"AQAB\"}}"
if string(out) != expected {
t.Error("Failed to marshal embedded non-pointer JWK properly:", string(out))
}

View File

@ -47,9 +47,9 @@ type JsonWebSignature struct {
// Signature represents a single signature over the JWS payload and protected header.
type Signature struct {
Header JoseHeader
Signature []byte
protected *rawHeader
header *rawHeader
signature []byte
original *rawSignatureInfo
}
@ -122,7 +122,7 @@ func (parsed *rawJsonWebSignature) sanitized() (*JsonWebSignature, error) {
}
signature.header = parsed.Header
signature.signature = parsed.Signature.bytes()
signature.Signature = parsed.Signature.bytes()
// Make a fake "original" rawSignatureInfo to store the unprocessed
// Protected header. This is necessary because the Protected header can
// contain arbitrary fields not registered as part of the spec. See
@ -151,7 +151,7 @@ func (parsed *rawJsonWebSignature) sanitized() (*JsonWebSignature, error) {
}
}
obj.Signatures[i].signature = sig.Signature.bytes()
obj.Signatures[i].Signature = sig.Signature.bytes()
// Copy value of sig
original := sig
@ -196,7 +196,7 @@ func parseSignedCompact(input string) (*JsonWebSignature, error) {
// CompactSerialize serializes an object using the compact serialization format.
func (obj JsonWebSignature) CompactSerialize() (string, error) {
if len(obj.Signatures) > 1 || obj.Signatures[0].header != nil {
if len(obj.Signatures) != 1 || obj.Signatures[0].header != nil || obj.Signatures[0].protected == nil {
return "", ErrNotSupported
}
@ -206,7 +206,7 @@ func (obj JsonWebSignature) CompactSerialize() (string, error) {
"%s.%s.%s",
base64URLEncode(serializedProtected),
base64URLEncode(obj.payload),
base64URLEncode(obj.Signatures[0].signature)), nil
base64URLEncode(obj.Signatures[0].Signature)), nil
}
// FullSerialize serializes an object using the full JSON serialization format.
@ -216,22 +216,42 @@ func (obj JsonWebSignature) FullSerialize() string {
}
if len(obj.Signatures) == 1 {
serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
raw.Protected = newBuffer(serializedProtected)
if obj.Signatures[0].protected != nil {
serializedProtected := mustSerializeJSON(obj.Signatures[0].protected)
raw.Protected = newBuffer(serializedProtected)
}
raw.Header = obj.Signatures[0].header
raw.Signature = newBuffer(obj.Signatures[0].signature)
raw.Signature = newBuffer(obj.Signatures[0].Signature)
} else {
raw.Signatures = make([]rawSignatureInfo, len(obj.Signatures))
for i, signature := range obj.Signatures {
serializedProtected := mustSerializeJSON(signature.protected)
raw.Signatures[i] = rawSignatureInfo{
Protected: newBuffer(serializedProtected),
Header: signature.header,
Signature: newBuffer(signature.signature),
Signature: newBuffer(signature.Signature),
}
if signature.protected != nil {
raw.Signatures[i].Protected = newBuffer(mustSerializeJSON(signature.protected))
}
}
}
return string(mustSerializeJSON(raw))
}
// MarshalJSON serializes the JWS to JSON.
func (obj JsonWebSignature) MarshalJSON() (result []byte, err error) {
return []byte(obj.FullSerialize()), nil
}
// UnmarshalJSON parses a JWS from JSON data. (This may also accept a compact
// JWS in a string.)
func (obj *JsonWebSignature) UnmarshalJSON(data []byte) (err error) {
parsedJWS, err := ParseSigned(string(data))
if err != nil {
return err
}
*obj = *parsedJWS
return nil
}

View File

@ -17,6 +17,7 @@
package jose
import (
"encoding/json"
"fmt"
"testing"
)
@ -256,3 +257,34 @@ func TestSampleNimbusJWSMessagesHMAC(t *testing.T) {
}
}
}
func TestMarshalUnmarshalJWS(t *testing.T) {
input := `{"jws":{"header":{"alg":"RS256","jwk":{"kty":"RSA","n":"7ixeydcbxxppzxrBphrW1atUiEZqTpiHDpI-79olav5XxAgWolHmVsJyxzoZXRxmtED8PF9-EICZWBGdSAL9ZTD0hLUCIsPcpdgT_LqNW3Sh2b2caPL2hbMF7vsXvnCGg9varpnHWuYTyRrCLUF9vM7ES-V3VCYTa7LcCSRm56Gg9r19qar43Z9kIKBBxpgt723v2cC4bmLmoAX2s217ou3uCpCXGLOeV_BesG4--Nl3pso1VhCfO85wEWjmW6lbv7Kg4d7Jdkv5DjDZfJ086fkEAYZVYGRpIgAvJBH3d3yKDCrSByUEud1bWuFjQBmMaeYOrVDXO_mbYg5PwUDMhw","e":"AQAB"}},"protected":"eyJub25jZSI6IjhISWVwVU5GWlVhLWV4S1RyWFZmNGcifQ","payload":"eyJjb250YWN0IjpbIm1haWx0bzpmb29AYmFyLmNvbSJdfQ","signature":"AyvVGMgXsQ1zTdXrZxE_gyO63pQgotL1KbI7gv6Wi8I7NRy0iAOkDAkWcTQT9pcCYApJ04lXfEDZfP5i0XgcFUm_6spxi5mFBZU-NemKcvK9dUiAbXvb4hB3GnaZtZiuVnMQUb_ku4DOaFFKbteA6gOYCnED_x7v0kAPHIYrQnvIa-KZ6pTajbV9348zgh9TL7NgGIIsTcMHd-Jatr4z1LQ0ubGa8tS300hoDhVzfoDQaEetYjCo1drR1RmdEN1SIzXdHOHfubjA3ZZRbrF_AJnNKpRRoIwzu1VayOhRmdy1qVSQZq_tENF4VrQFycEL7DhG7JLoXC4T2p1urwMlsw"}}`
parsed := struct {
JWS *JsonWebSignature `json:"jws"`
}{}
err := json.Unmarshal([]byte(input), &parsed)
if err != nil {
t.Error("unable to unmarshal JSON JWS")
}
if parsed.JWS == nil {
t.Error("JWS did not correctly unmarshal")
}
serialized, err := json.Marshal(parsed)
if err != nil {
t.Error("unable to marshal JSON JWS")
}
err = json.Unmarshal(serialized, &parsed)
if err != nil {
t.Error("unable to unmarshal marshaled JSON JWS")
}
if parsed.JWS == nil {
t.Error("JWS did not correctly unmarshal from marshaled JSON JWS")
}
}

View File

@ -129,6 +129,7 @@ type JoseHeader struct {
KeyID string
JsonWebKey *JsonWebKey
Nonce string
Algorithm string
}
// sanitized produces a cleaned-up header object from the raw JSON.
@ -137,6 +138,7 @@ func (parsed rawHeader) sanitized() JoseHeader {
KeyID: parsed.Kid,
JsonWebKey: parsed.Jwk,
Nonce: parsed.Nonce,
Algorithm: parsed.Alg,
}
}

View File

@ -175,7 +175,7 @@ func (obj JsonWebSignature) Verify(verificationKey interface{}) ([]byte, JoseHea
}
input := obj.computeAuthData(&signature)
alg := SignatureAlgorithm(headers.Alg)
err := verifier.verifyPayload(input, signature.signature, alg)
err := verifier.verifyPayload(input, signature.Signature, alg)
if err == nil {
return obj.payload, headers.sanitized(), nil
}

View File

@ -114,11 +114,11 @@ func TestRoundtripsJWSCorruptSignature(t *testing.T) {
corrupters := []func(*JsonWebSignature){
func(obj *JsonWebSignature) {
// Changes bytes in signature
obj.Signatures[0].signature[10]++
obj.Signatures[0].Signature[10]++
},
func(obj *JsonWebSignature) {
// Set totally invalid signature
obj.Signatures[0].signature = []byte("###")
obj.Signatures[0].Signature = []byte("###")
},
}

View File

@ -301,7 +301,7 @@ func (ctx symmetricMac) signPayload(payload []byte, alg SignatureAlgorithm) (Sig
}
return Signature{
signature: mac,
Signature: mac,
protected: &rawHeader{},
}, nil
}

View File

@ -37,6 +37,7 @@ A not-so-up-to-date-list-that-may-be-actually-current:
* https://github.com/StalkR/dns-reverse-proxy
* https://github.com/tianon/rawdns
* https://mesosphere.github.io/mesos-dns/
* https://pulse.turbobytes.com/
Send pull request if you want to be listed here.

View File

@ -189,26 +189,15 @@ func (c *Client) exchange(m *Msg, a string) (r *Msg, rtt time.Duration, err erro
// If the received message contains a TSIG record the transaction
// signature is verified.
func (co *Conn) ReadMsg() (*Msg, error) {
var p []byte
m := new(Msg)
if _, ok := co.Conn.(*net.TCPConn); ok {
p = make([]byte, MaxMsgSize)
} else {
if co.UDPSize > MinMsgSize {
p = make([]byte, co.UDPSize)
} else {
p = make([]byte, MinMsgSize)
}
}
n, err := co.Read(p)
if err != nil && n == 0 {
p, err := co.ReadMsgHeader(nil)
if err != nil {
return nil, err
}
p = p[:n]
m := new(Msg)
if err := m.Unpack(p); err != nil {
return nil, err
}
co.rtt = time.Since(co.t)
if t := m.IsTsig(); t != nil {
if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
return m, ErrSecret
@ -219,6 +208,81 @@ func (co *Conn) ReadMsg() (*Msg, error) {
return m, err
}
// ReadMsgHeader reads a DNS message, parses and populates hdr (when hdr is not nil).
// Returns message as a byte slice to be parsed with Msg.Unpack later on.
// Note that error handling on the message body is not possible as only the header is parsed.
func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
var (
p []byte
n int
err error
)
if t, ok := co.Conn.(*net.TCPConn); ok {
// First two bytes specify the length of the entire message.
l, err := tcpMsgLen(t)
if err != nil {
return nil, err
}
p = make([]byte, l)
n, err = tcpRead(t, p)
} else {
if co.UDPSize > MinMsgSize {
p = make([]byte, co.UDPSize)
} else {
p = make([]byte, MinMsgSize)
}
n, err = co.Read(p)
}
if err != nil {
return nil, err
} else if n < headerSize {
return nil, ErrShortRead
}
p = p[:n]
if hdr != nil {
if _, err = UnpackStruct(hdr, p, 0); err != nil {
return nil, err
}
}
return p, err
}
// tcpMsgLen is a helper func to read first two bytes of stream as uint16 packet length.
func tcpMsgLen(t *net.TCPConn) (int, error) {
p := []byte{0, 0}
n, err := t.Read(p)
if err != nil {
return 0, err
}
if n != 2 {
return 0, ErrShortRead
}
l, _ := unpackUint16(p, 0)
if l == 0 {
return 0, ErrShortRead
}
return int(l), nil
}
// tcpRead calls TCPConn.Read enough times to fill allocated buffer.
func tcpRead(t *net.TCPConn, p []byte) (int, error) {
n, err := t.Read(p)
if err != nil {
return n, err
}
for n < len(p) {
j, err := t.Read(p[n:])
if err != nil {
return n, err
}
n += j
}
return n, err
}
// Read implements the net.Conn read method.
func (co *Conn) Read(p []byte) (n int, err error) {
if co.Conn == nil {
@ -228,37 +292,22 @@ func (co *Conn) Read(p []byte) (n int, err error) {
return 0, io.ErrShortBuffer
}
if t, ok := co.Conn.(*net.TCPConn); ok {
n, err = t.Read(p[0:2])
if err != nil || n != 2 {
return n, err
l, err := tcpMsgLen(t)
if err != nil {
return 0, err
}
l, _ := unpackUint16(p[0:2], 0)
if l == 0 {
return 0, ErrShortRead
}
if int(l) > len(p) {
if l > len(p) {
return int(l), io.ErrShortBuffer
}
n, err = t.Read(p[:l])
if err != nil {
return n, err
}
i := n
for i < int(l) {
j, err := t.Read(p[i:int(l)])
if err != nil {
return i, err
}
i += j
}
n = i
return n, err
return tcpRead(t, p[:l])
}
// UDP connection
n, err = co.Conn.Read(p)
if err != nil {
return n, err
}
co.rtt = time.Since(co.t)
return n, err
}

View File

@ -32,7 +32,7 @@ func TestClientSync(t *testing.T) {
if err != nil {
t.Errorf("failed to exchange: %v", err)
}
if r != nil && r.Rcode != RcodeSuccess {
if r == nil || r.Rcode != RcodeSuccess {
t.Errorf("failed to get an valid answer\n%v", r)
}
}
@ -235,3 +235,52 @@ func ExampleUpdateLeaseTSIG(t *testing.T) {
t.Error(err)
}
}
func TestClientConn(t *testing.T) {
HandleFunc("miek.nl.", HelloServer)
defer HandleRemove("miek.nl.")
// This uses TCP just to make it slightly different than TestClientSync
s, addrstr, err := RunLocalTCPServer("127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to run test server: %v", err)
}
defer s.Shutdown()
m := new(Msg)
m.SetQuestion("miek.nl.", TypeSOA)
cn, err := Dial("tcp", addrstr)
if err != nil {
t.Errorf("failed to dial %s: %v", addrstr, err)
}
err = cn.WriteMsg(m)
if err != nil {
t.Errorf("failed to exchange: %v", err)
}
r, err := cn.ReadMsg()
if r == nil || r.Rcode != RcodeSuccess {
t.Errorf("failed to get an valid answer\n%v", r)
}
err = cn.WriteMsg(m)
if err != nil {
t.Errorf("failed to exchange: %v", err)
}
h := new(Header)
buf, err := cn.ReadMsgHeader(h)
if buf == nil {
t.Errorf("failed to get an valid answer\n%v", r)
}
if int(h.Bits&0xF) != RcodeSuccess {
t.Errorf("failed to get an valid answer in ReadMsgHeader\n%v", r)
}
if h.Ancount != 0 || h.Qdcount != 1 || h.Nscount != 0 || h.Arcount != 1 {
t.Errorf("expected to have question and additional in response; got something else: %+v", h)
}
if err = r.Unpack(buf); err != nil {
t.Errorf("unable to unpack message fully: %v", err)
}
}

View File

@ -202,6 +202,9 @@ RFC 6895 sets aside a range of type codes for private use. This range
is 65,280 - 65,534 (0xFF00 - 0xFFFE). When experimenting with new Resource Records these
can be used, before requesting an official type code from IANA.
see http://miek.nl/posts/2014/Sep/21/Private%20RRs%20and%20IDN%20in%20Go%20DNS/ for more
information.
EDNS0
EDNS0 is an extension mechanism for the DNS defined in RFC 2671 and updated

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,10 @@ package idn
import (
"bytes"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
"strings"
"unicode"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
// Implementation idea from RFC itself and from from IDNA::Punycode created by
@ -26,8 +27,8 @@ const (
)
// ToPunycode converts unicode domain names to DNS-appropriate punycode names.
// This function would return incorrect result for strings for non-canonical
// unicode strings.
// This function would return an empty string result for domain names with
// invalid unicode strings. This function expects domain names in lowercase.
func ToPunycode(s string) string {
tokens := dns.SplitDomainName(s)
switch {
@ -40,7 +41,11 @@ func ToPunycode(s string) string {
}
for i := range tokens {
tokens[i] = string(encode([]byte(tokens[i])))
t := encode([]byte(tokens[i]))
if t == nil {
return ""
}
tokens[i] = string(t)
}
return strings.Join(tokens, ".")
}
@ -138,12 +143,18 @@ func tfunc(k, bias rune) rune {
return k - bias
}
// encode transforms Unicode input bytes (that represent DNS label) into punycode bytestream
// encode transforms Unicode input bytes (that represent DNS label) into
// punycode bytestream. This function would return nil if there's an invalid
// character in the label.
func encode(input []byte) []byte {
n, bias := _N, _BIAS
b := bytes.Runes(input)
for i := range b {
if !isValidRune(b[i]) {
return nil
}
b[i] = preprune(b[i])
}
@ -267,3 +278,34 @@ func decode(b []byte) []byte {
}
return ret.Bytes()
}
// isValidRune checks if the character is valid. We will look for the
// character property in the code points list. For now we aren't checking special
// rules in case of contextual property
func isValidRune(r rune) bool {
return findProperty(r) == propertyPVALID
}
// findProperty will try to check the code point property of the given
// character. It will use a binary search algorithm as we have a slice of
// ordered ranges (average case performance O(log n))
func findProperty(r rune) property {
imin, imax := 0, len(codePoints)
for imax >= imin {
imid := (imin + imax) / 2
codePoint := codePoints[imid]
if (codePoint.start == r && codePoint.end == 0) || (codePoint.start <= r && codePoint.end >= r) {
return codePoint.state
}
if (codePoint.end > 0 && codePoint.end < r) || (codePoint.end == 0 && codePoint.start < r) {
imin = imid + 1
} else {
imax = imid - 1
}
}
return propertyUnknown
}

View File

@ -13,13 +13,13 @@ var testcases = [][2]string{
{"AbC", "abc"},
{"я", "xn--41a"},
{"zя", "xn--z-0ub"},
{"ЯZ", "xn--z-zub"},
{"яZ", "xn--z-zub"},
{"а-я", "xn----7sb8g"},
{"إختبار", "xn--kgbechtv"},
{"آزمایشی", "xn--hgbk6aj7f53bba"},
{"测试", "xn--0zwm56d"},
{"測試", "xn--g6w251d"},
{"Испытание", "xn--80akhbyknj4f"},
{"испытание", "xn--80akhbyknj4f"},
{"परीक्षा", "xn--11b5bs3a9aj6g"},
{"δοκιμή", "xn--jxalpdlp"},
{"테스트", "xn--9t4b11yi5a"},
@ -27,6 +27,7 @@ var testcases = [][2]string{
{"テスト", "xn--zckzah"},
{"பரிட்சை", "xn--hlcj6aya9esc7a"},
{"mamão-com-açúcar", "xn--mamo-com-acar-yeb1e6q"},
{"σ", "xn--4xa"},
}
func TestEncodeDecodePunycode(t *testing.T) {
@ -81,17 +82,34 @@ func TestEncodeDecodeFinalPeriod(t *testing.T) {
}
}
var invalid = []string{
var invalidACEs = []string{
"xn--*",
"xn--",
"xn---",
}
func TestInvalidPunycode(t *testing.T) {
for _, d := range invalid {
for _, d := range invalidACEs {
s := FromPunycode(d)
if s != d {
t.Errorf("Changed invalid name %s to %#v", d, s)
}
}
}
// You can verify the labels that are valid or not comparing to the Verisign
// website: http://mct.verisign-grs.com/
var invalidUnicodes = []string{
"Σ",
"ЯZ",
"Испытание",
}
func TestInvalidUnicodes(t *testing.T) {
for _, d := range invalidUnicodes {
s := ToPunycode(d)
if s != "" {
t.Errorf("Changed invalid name %s to %#v", d, s)
}
}
}

View File

@ -921,7 +921,7 @@ func packStructValue(val reflect.Value, msg []byte, off int, compression map[str
copy(msg[off:off+len(s)], s)
off += len(s)
case `dns:"octet"`:
bytesTmp := make([]byte, 0)
bytesTmp := make([]byte, 256)
off, err = packOctetString(fv.String(), msg, off, bytesTmp)
if err != nil {
return lenmsg, err

View File

@ -1477,3 +1477,32 @@ func TestParseCAA(t *testing.T) {
}
}
}
func TestPackCAA(t *testing.T) {
m := new(Msg)
record := new(CAA)
record.Hdr = RR_Header{Name: "example.com.", Rrtype: TypeCAA, Class: ClassINET, Ttl: 0}
record.Tag = "issue"
record.Value = "symantec.com"
record.Flag = 1
m.Answer = append(m.Answer, record)
bytes, err := m.Pack()
if err != nil {
t.Fatalf("failed to pack msg: %v", err)
}
if err := m.Unpack(bytes); err != nil {
t.Fatalf("failed to unpack msg: %v", err)
}
if len(m.Answer) != 1 {
t.Fatalf("incorrect number of answers unpacked")
}
rr := m.Answer[0].(*CAA)
if rr.Tag != "issue" {
t.Fatalf("invalid tag for unpacked answer")
} else if rr.Value != "symantec.com" {
t.Fatalf("invalid value for unpacked answer")
} else if rr.Flag != 1 {
t.Fatalf("invalid flag for unpacked answer")
}
}

View File

@ -158,6 +158,8 @@ type Header struct {
}
const (
headerSize = 12
// Header.Bits
_QR = 1 << 15 // query/response (response=1)
_AA = 1 << 10 // authoritative
@ -1567,9 +1569,10 @@ type CAA struct {
func (rr *CAA) Header() *RR_Header { return &rr.Hdr }
func (rr *CAA) copy() RR { return &CAA{*rr.Hdr.copyHeader(), rr.Flag, rr.Tag, rr.Value} }
func (rr *CAA) len() int { return rr.Hdr.len() + 1 + len(rr.Tag) + len(rr.Value)/2 }
func (rr *CAA) String() string { return rr.Hdr.String() + strconv.Itoa(int(rr.Flag)) + " " + rr.Tag + " " + sprintCAAValue(rr.Value) }
func (rr *CAA) len() int { return rr.Hdr.len() + 2 + len(rr.Tag) + len(rr.Value) }
func (rr *CAA) String() string {
return rr.Hdr.String() + strconv.Itoa(int(rr.Flag)) + " " + rr.Tag + " " + sprintCAAValue(rr.Value)
}
type UID struct {
Hdr RR_Header

View File

@ -1,21 +1,29 @@
# This Makefile also tricks Travis into not running 'go get' for our
# build. See http://docs.travis-ci.com/user/languages/go/
OBJDIR = ./bin
OBJDIR ?= ./bin
DESTDIR ?= /usr/local/bin
ARCHIVEDIR ?= /tmp
VERSION ?= 1.0.0
EPOCH ?= 1
MAINTAINER ?= "Community"
OBJECTS = activity-monitor \
admin-revoker \
boulder \
boulder-ca \
boulder-ra \
boulder-sa \
boulder-va \
boulder-wfe \
expiration-mailer \
ocsp-updater \
ocsp-responder
# Build environment variables (referencing core/util.go)
BUILD_ID = $(shell git symbolic-ref --short HEAD 2>/dev/null) +$(shell git rev-parse --short HEAD)
COMMIT_ID = $(shell git rev-parse --short HEAD)
BUILD_ID = $(shell git symbolic-ref --short HEAD 2>/dev/null) +$(COMMIT_ID)
BUILD_ID_VAR = github.com/letsencrypt/boulder/core.BuildID
BUILD_HOST = $(shell whoami)@$(shell hostname)
@ -37,7 +45,7 @@ pre:
# Compile each of the binaries
$(OBJECTS): pre
@echo [go] bin/$@
@go build -tags pkcs11 -o ./bin/$@ -ldflags \
@go build -o ./bin/$@ -ldflags \
"-X $(BUILD_ID_VAR) '$(BUILD_ID)' -X $(BUILD_TIME_VAR) '$(BUILD_TIME)' \
-X $(BUILD_HOST_VAR) '$(BUILD_HOST)'" \
cmd/$@/main.go
@ -45,3 +53,33 @@ $(OBJECTS): pre
clean:
rm -f $(OBJDIR)/*
rmdir $(OBJDIR)
# Install to a destination directory. Defaults to /usr/local/, but you can
# override it with the DESTDIR variable. Example:
#
# DESTDIR=~/bin make install
install:
@mkdir -p $(DESTDIR)
$(foreach var,$(OBJECTS), install -m 0755 $(OBJDIR)/$(var) $(DESTDIR)/;)
# Produce a tarball of the current commit; you can set the destination in the
# ARCHIVEDIR variable.
archive:
git archive --output=$(ARCHIVEDIR)/boulder-$(COMMIT_ID).tar.gz \
--prefix=boulder-$(COMMIT_ID)/ $(COMMIT_ID)
# Building an RPM requires `fpm` from https://github.com/jordansissel/fpm
# which you can install with `gem install fpm`.
# It is recommended that maintainers use environment overrides to specify
# Version and Epoch, such as:
#
# VERSION=0.1.9 EPOCH=52 MAINTAINER="$(whoami)" ARCHIVEDIR=/tmp make build rpm
rpm:
fpm -s dir -t rpm --rpm-digest sha256 --name "boulder" \
--license "Mozilla Public License v2.0" --vendor "ISRG" \
--url "https://github.com/letsencrypt/boulder" --prefix=/opt/boulder \
--version $(VERSION) --iteration $(COMMIT_ID) --epoch $(EPOCH) \
--package $(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).x86_64.rpm \
--description "Boulder is an ACME-compatible X.509 Certificate Authority" \
--depends "libtool-ltdl" --maintainer "$(MAINTAINER)" \
test/boulder-config.json $(foreach var,$(OBJECTS), $(OBJDIR)/$(var))

View File

@ -30,6 +30,13 @@ A quick-start method for running a Boulder instance is to use one of the example
> docker run --name=boulder --read-only=true --rm=true -v $(pwd)/.boulder-config:/boulder:ro -p 4000:4000 quay.io/letsencrypt/boulder:latest boulder
```
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:
```
@ -37,6 +44,7 @@ To run a single module, specifying the AMQP server, you might use something more
```
Quickstart
----------
@ -55,6 +63,8 @@ CentOS:
OS X:
`sudo port install libtool` or `brew install libtool`
(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 github.com/letsencrypt/boulder # Ignore errors about no buildable files
> cd $GOPATH/src/github.com/letsencrypt/boulder
@ -115,31 +125,22 @@ easier](https://groups.google.com/forum/m/#!topic/golang-dev/nMWoEAG55v8)
and to [avoid insecure fallback in go
get](https://github.com/golang/go/issues/9637).
We need to use the build tag 'pkcs11' to really pull in all our dependencies.
To do this, you'll need to pull and install this godep branch, which supports
build tags: https://github.com/tools/godep/pull/117/files. NOTE: If you skip
this step, godep will delete some of the vendorized dependencies.
To update dependencies:
```
# Disable insecure fallback by blocking port 80.
sudo /sbin/iptables -A OUTPUT -p tcp --dport 80 -j DROP
# Fetch godep
go get https://github.com/tools/godep.git
# Pull in the tags branch and install
cd $GOPATH/src/github.com/tools/godep
git pull https://github.com/jnfeinstein/godep.git jnfeinstein
go install
go get -u https://github.com/tools/godep
# Update to the latest version of a dependency. Alternately you can cd to the
# directory under GOPATH and check out a specific revision.
# directory under GOPATH and check out a specific revision. Here's an example
# using cfssl:
go get -u github.com/cloudflare/cfssl/...
# Update the Godep config to the appropriate version.
godep update github.com/cloudflare/cfssl/...
# Save the dependencies, rewriting any internal or external dependencies that
# may have been added.
godep save -r -tags pkcs11 ./...
godep save -r ./...
git add Godeps
git commit
# Assuming you had no other iptables rules, re-enable port 80.

View File

@ -32,7 +32,7 @@ type Config struct {
Profile string
TestMode bool
DBDriver string
DBName string
DBConnect string
SerialPrefix int
Key KeyConfig
// LifespanOCSP is how long OCSP responses are valid for; It should be longer
@ -44,6 +44,9 @@ type Config struct {
// The maximum number of subjectAltNames in a single certificate
MaxNames int
CFSSL cfsslConfig.Config
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
// KeyConfig should contain either a File path to a PEM-format private key,
@ -471,7 +474,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
return cert, nil
}
ca.SA.UpdateOCSP(serial, ocspResponse)
err = ca.SA.UpdateOCSP(serial, ocspResponse)
if err != nil {
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed storing: %s", err))
return cert, nil

View File

@ -18,6 +18,7 @@ import (
cfsslConfig "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
ocspConfig "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp/config"
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/sa"
@ -331,6 +332,8 @@ var BadAlgorithmCSRhex = "308202aa30820192020100300d310b300906035504061302" +
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)
var log = mocks.UseMockLog()
// CFSSL config
const profileName = "ee"
const caKeyFile = "../test/test-ca.key"
@ -348,7 +351,7 @@ func setup(t *testing.T) (cadb core.CertificateAuthorityDatabase, storageAuthori
ssa.CreateTablesIfNotExists()
storageAuthority = ssa
cadb, _ = test.NewMockCertificateAuthorityDatabase()
cadb, _ = mocks.NewMockCertificateAuthorityDatabase()
// Create a CA
caConfig = Config{
@ -433,8 +436,9 @@ func TestRevoke(t *testing.T) {
test.AssertNotError(t, err, "Failed to get cert status")
test.AssertEquals(t, status.Status, core.OCSPStatusRevoked)
test.Assert(t, time.Now().Sub(status.OCSPLastUpdated) > time.Second,
fmt.Sprintf("OCSP LastUpdated was wrong: %v", status.OCSPLastUpdated))
secondAgo := time.Now().Add(-time.Second)
test.Assert(t, status.OCSPLastUpdated.After(secondAgo),
fmt.Sprintf("OCSP LastUpdated was more than a second old: %v", status.OCSPLastUpdated))
}
func TestIssueCertificate(t *testing.T) {

View File

@ -88,19 +88,7 @@ func startMonitor(rpcCh *amqp.Channel, logger *blog.AuditLogger, stats statsd.St
cmd.FailOnError(err, "Could not determine hostname")
}
err = rpcCh.ExchangeDeclare(
AmqpExchange,
AmqpExchangeType,
AmqpDurable,
AmqpDeleteUnused,
AmqpInternal,
AmqpNoWait,
nil)
if err != nil {
cmd.FailOnError(err, "Could not declare exchange")
}
_, err = rpcCh.QueueDeclare(
_, err = rpcCh.QueueDeclarePassive(
QueueName,
AmqpDurable,
AmqpDeleteUnused,
@ -108,17 +96,32 @@ func startMonitor(rpcCh *amqp.Channel, logger *blog.AuditLogger, stats statsd.St
AmqpNoWait,
nil)
if err != nil {
cmd.FailOnError(err, "Could not declare queue")
}
logger.Info(fmt.Sprintf("Queue %s does not exist on AMQP server, attempting to create.", QueueName))
err = rpcCh.QueueBind(
QueueName,
"#", //wildcard
AmqpExchange,
false,
nil)
if err != nil {
cmd.FailOnError(err, "Could not bind queue")
// Attempt to create the Queue if not exists
_, err = rpcCh.QueueDeclare(
QueueName,
AmqpDurable,
AmqpDeleteUnused,
AmqpExclusive,
AmqpNoWait,
nil)
if err != nil {
cmd.FailOnError(err, "Could not declare queue")
}
routingKey := "#" //wildcard
err = rpcCh.QueueBind(
QueueName,
routingKey,
AmqpExchange,
false,
nil)
if err != nil {
txt := fmt.Sprintf("Could not bind to queue [%s]. NOTE: You may need to delete %s to re-trigger the bind attempt after fixing permissions, or manually bind the queue to %s.", QueueName, QueueName, routingKey)
cmd.FailOnError(err, txt)
}
}
deliveries, err := rpcCh.Consume(
@ -165,7 +168,9 @@ func main() {
blog.SetAuditLogger(auditlogger)
ch, err := cmd.AmqpChannel(c)
go cmd.DebugServer(c.ActivityMonitor.DebugAddr)
ch, err := rpc.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")

View File

@ -56,7 +56,7 @@ func loadConfig(c *cli.Context) (config cmd.Config, err error) {
return
}
func setupContext(context *cli.Context) (rpc.CertificateAuthorityClient, *blog.AuditLogger, *gorp.DbMap) {
func setupContext(context *cli.Context) (rpc.CertificateAuthorityClient, *blog.AuditLogger, *gorp.DbMap, rpc.StorageAuthorityClient) {
c, err := loadConfig(context)
cmd.FailOnError(err, "Failed to load Boulder configuration")
@ -67,7 +67,7 @@ func setupContext(context *cli.Context) (rpc.CertificateAuthorityClient, *blog.A
cmd.FailOnError(err, "Could not connect to Syslog")
blog.SetAuditLogger(auditlogger)
ch, err := cmd.AmqpChannel(c)
ch, err := rpc.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
caRPC, err := rpc.NewAmqpRPCClient("revoker->CA", c.AMQP.CA.Server, ch)
@ -76,10 +76,16 @@ func setupContext(context *cli.Context) (rpc.CertificateAuthorityClient, *blog.A
cac, err := rpc.NewCertificateAuthorityClient(caRPC)
cmd.FailOnError(err, "Unable to create CA client")
dbMap, err := sa.NewDbMap(c.Revoker.DBDriver, c.Revoker.DBName)
dbMap, err := sa.NewDbMap(c.Revoker.DBDriver, c.Revoker.DBConnect)
cmd.FailOnError(err, "Couldn't setup database connection")
return cac, auditlogger, dbMap
saRPC, err := rpc.NewAmqpRPCClient("AdminRevoker->SA", c.AMQP.SA.Server, ch)
cmd.FailOnError(err, "Unable to create RPC client")
sac, err := rpc.NewStorageAuthorityClient(saRPC)
cmd.FailOnError(err, "Failed to create SA client")
return cac, auditlogger, dbMap, sac
}
func addDeniedNames(tx *gorp.Transaction, names []string) (err error) {
@ -126,12 +132,7 @@ func revokeBySerial(serial string, reasonCode int, deny bool, cac rpc.Certificat
return
}
func revokeByReg(regID int, reasonCode int, deny bool, cac rpc.CertificateAuthorityClient, auditlogger *blog.AuditLogger, tx *gorp.Transaction) (err error) {
_, err = tx.Get(core.Registration{}, regID)
if err != nil {
return
}
func revokeByReg(regID int64, reasonCode int, deny bool, cac rpc.CertificateAuthorityClient, auditlogger *blog.AuditLogger, tx *gorp.Transaction) (err error) {
var certs []core.Certificate
_, err = tx.Select(&certs, "SELECT serial FROM certificates WHERE registrationID = :regID", map[string]interface{}{"regID": regID})
if err != nil {
@ -163,7 +164,7 @@ func main() {
Usage: "Path to Boulder JSON configuration file",
},
cli.BoolFlag{
Name: "deny-future",
Name: "deny",
Usage: "Add certificate DNS names to the denied list",
},
}
@ -178,7 +179,7 @@ func main() {
cmd.FailOnError(err, "Reason code argument must be a integer")
deny := c.GlobalBool("deny")
cac, auditlogger, dbMap := setupContext(c)
cac, auditlogger, dbMap, _ := setupContext(c)
tx, err := dbMap.Begin()
if err != nil {
@ -201,13 +202,13 @@ func main() {
Usage: "Revoke all certificates associated with a registration ID",
Action: func(c *cli.Context) {
// 1: registration ID, 2: reasonCode (3: deny flag)
regID, err := strconv.Atoi(c.Args().First())
regID, err := strconv.ParseInt(c.Args().First(), 10, 64)
cmd.FailOnError(err, "Registration ID argument must be a integer")
reasonCode, err := strconv.Atoi(c.Args().Get(1))
cmd.FailOnError(err, "Reason code argument must be a integer")
deny := c.GlobalBool("deny")
cac, auditlogger, dbMap := setupContext(c)
cac, auditlogger, dbMap, sac := setupContext(c)
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer auditlogger.AuditPanic()
@ -217,6 +218,11 @@ func main() {
}
cmd.FailOnError(err, "Couldn't begin transaction")
_, err = sac.GetRegistration(regID)
if err != nil {
cmd.FailOnError(err, "Couldn't fetch registration")
}
err = revokeByReg(regID, reasonCode, deny, cac, auditlogger, tx)
if err != nil {
tx.Rollback()

View File

@ -7,7 +7,6 @@ package main
import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
"github.com/letsencrypt/boulder/ca"
"github.com/letsencrypt/boulder/cmd"
@ -30,7 +29,10 @@ func main() {
blog.SetAuditLogger(auditlogger)
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(c.CA.DBDriver, c.CA.DBName)
go cmd.DebugServer(c.CA.DebugAddr)
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(c.CA.DBDriver, c.CA.DBConnect)
cmd.FailOnError(err, "Failed to create CA database")
if c.SQL.CreateTables {
@ -44,29 +46,24 @@ func main() {
go cmd.ProfileCmd("CA", stats)
for {
ch, err := cmd.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
closeChan := ch.NotifyClose(make(chan *amqp.Error, 1))
saRPC, err := rpc.NewAmqpRPCClient("CA->SA", c.AMQP.SA.Server, ch)
connectionHandler := func(srv *rpc.AmqpRPCServer) {
saRPC, err := rpc.NewAmqpRPCClient("CA->SA", c.AMQP.SA.Server, srv.Channel)
cmd.FailOnError(err, "Unable to create RPC client")
sac, err := rpc.NewStorageAuthorityClient(saRPC)
cmd.FailOnError(err, "Failed to create SA client")
cai.SA = &sac
cas := rpc.NewAmqpRPCServer(c.AMQP.CA.Server, ch)
err = rpc.NewCertificateAuthorityServer(cas, cai)
cmd.FailOnError(err, "Unable to create CA server")
auditlogger.Info(app.VersionString())
cmd.RunUntilSignaled(auditlogger, cas, closeChan)
}
cas, err := rpc.NewAmqpRPCServer(c.AMQP.CA.Server, connectionHandler)
cmd.FailOnError(err, "Unable to create CA RPC server")
rpc.NewCertificateAuthorityServer(cas, cai)
auditlogger.Info(app.VersionString())
err = cas.Start(c)
cmd.FailOnError(err, "Unable to run CA RPC server")
}
app.Run()

View File

@ -6,8 +6,10 @@
package main
import (
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/cmd"
blog "github.com/letsencrypt/boulder/log"
@ -31,26 +33,26 @@ func main() {
blog.SetAuditLogger(auditlogger)
go cmd.DebugServer(c.RA.DebugAddr)
rai := ra.NewRegistrationAuthorityImpl()
rai.AuthzBase = c.Common.BaseURL + wfe.AuthzPath
rai.MaxKeySize = c.Common.MaxKeySize
rai.Stats = stats
raDNSTimeout, err := time.ParseDuration(c.Common.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse RA DNS timeout")
rai.DNSResolver = core.NewDNSResolverImpl(raDNSTimeout, []string{c.Common.DNSResolver})
go cmd.ProfileCmd("RA", stats)
for {
ch, err := cmd.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
closeChan := ch.NotifyClose(make(chan *amqp.Error, 1))
vaRPC, err := rpc.NewAmqpRPCClient("RA->VA", c.AMQP.VA.Server, ch)
connectionHandler := func(srv *rpc.AmqpRPCServer) {
vaRPC, err := rpc.NewAmqpRPCClient("RA->VA", c.AMQP.VA.Server, srv.Channel)
cmd.FailOnError(err, "Unable to create RPC client")
caRPC, err := rpc.NewAmqpRPCClient("RA->CA", c.AMQP.CA.Server, ch)
caRPC, err := rpc.NewAmqpRPCClient("RA->CA", c.AMQP.CA.Server, srv.Channel)
cmd.FailOnError(err, "Unable to create RPC client")
saRPC, err := rpc.NewAmqpRPCClient("RA->SA", c.AMQP.SA.Server, ch)
saRPC, err := rpc.NewAmqpRPCClient("RA->SA", c.AMQP.SA.Server, srv.Channel)
cmd.FailOnError(err, "Unable to create RPC client")
vac, err := rpc.NewValidationAuthorityClient(vaRPC)
@ -65,17 +67,16 @@ func main() {
rai.VA = &vac
rai.CA = &cac
rai.SA = &sac
ras := rpc.NewAmqpRPCServer(c.AMQP.RA.Server, ch)
err = rpc.NewRegistrationAuthorityServer(ras, &rai)
cmd.FailOnError(err, "Unable to create RA server")
auditlogger.Info(app.VersionString())
cmd.RunUntilSignaled(auditlogger, ras, closeChan)
}
ras, err := rpc.NewAmqpRPCServer(c.AMQP.RA.Server, connectionHandler)
cmd.FailOnError(err, "Unable to create RA RPC server")
rpc.NewRegistrationAuthorityServer(ras, &rai)
auditlogger.Info(app.VersionString())
err = ras.Start(c)
cmd.FailOnError(err, "Unable to run RA RPC server")
}
app.Run()

View File

@ -7,7 +7,6 @@ package main
import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
"github.com/letsencrypt/boulder/cmd"
blog "github.com/letsencrypt/boulder/log"
@ -30,7 +29,10 @@ func main() {
blog.SetAuditLogger(auditlogger)
sai, err := sa.NewSQLStorageAuthority(c.SA.DBDriver, c.SA.DBName)
go cmd.DebugServer(c.SA.DebugAddr)
sai, err := sa.NewSQLStorageAuthority(c.SA.DBDriver, c.SA.DBConnect)
cmd.FailOnError(err, "Failed to create SA impl")
sai.SetSQLDebug(c.SQL.SQLDebug)
@ -41,21 +43,16 @@ func main() {
go cmd.ProfileCmd("SA", stats)
for {
ch, err := cmd.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
connectionHandler := func(*rpc.AmqpRPCServer) {}
closeChan := ch.NotifyClose(make(chan *amqp.Error, 1))
sas, err := rpc.NewAmqpRPCServer(c.AMQP.SA.Server, connectionHandler)
cmd.FailOnError(err, "Unable to create SA RPC server")
rpc.NewStorageAuthorityServer(sas, sai)
sas := rpc.NewAmqpRPCServer(c.AMQP.SA.Server, ch)
auditlogger.Info(app.VersionString())
err = rpc.NewStorageAuthorityServer(sas, sai)
cmd.FailOnError(err, "Could create SA RPC server")
auditlogger.Info(app.VersionString())
cmd.RunUntilSignaled(auditlogger, sas, closeChan)
}
err = sas.Start(c)
cmd.FailOnError(err, "Unable to run SA RPC server")
}
app.Run()

View File

@ -9,7 +9,6 @@ import (
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
@ -33,38 +32,35 @@ func main() {
blog.SetAuditLogger(auditlogger)
go cmd.DebugServer(c.VA.DebugAddr)
go cmd.ProfileCmd("VA", stats)
vai := va.NewValidationAuthorityImpl(c.CA.TestMode)
vai.Stats = stats
dnsTimeout, err := time.ParseDuration(c.VA.DNSTimeout)
dnsTimeout, err := time.ParseDuration(c.Common.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse DNS timeout")
vai.DNSResolver = core.NewDNSResolver(dnsTimeout, []string{c.VA.DNSResolver})
vai.DNSResolver = core.NewDNSResolverImpl(dnsTimeout, []string{c.Common.DNSResolver})
vai.UserAgent = c.VA.UserAgent
for {
ch, err := cmd.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
closeChan := ch.NotifyClose(make(chan *amqp.Error, 1))
raRPC, err := rpc.NewAmqpRPCClient("VA->RA", c.AMQP.RA.Server, ch)
connectionHandler := func(srv *rpc.AmqpRPCServer) {
raRPC, err := rpc.NewAmqpRPCClient("VA->RA", c.AMQP.RA.Server, srv.Channel)
cmd.FailOnError(err, "Unable to create RPC client")
rac, err := rpc.NewRegistrationAuthorityClient(raRPC)
cmd.FailOnError(err, "Unable to create RA client")
vai.RA = &rac
vas := rpc.NewAmqpRPCServer(c.AMQP.VA.Server, ch)
err = rpc.NewValidationAuthorityServer(vas, &vai)
cmd.FailOnError(err, "Unable to create VA server")
auditlogger.Info(app.VersionString())
cmd.RunUntilSignaled(auditlogger, vas, closeChan)
}
vas, err := rpc.NewAmqpRPCServer(c.AMQP.VA.Server, connectionHandler)
cmd.FailOnError(err, "Unable to create VA RPC server")
rpc.NewValidationAuthorityServer(vas, &vai)
auditlogger.Info(app.VersionString())
err = vas.Start(c)
cmd.FailOnError(err, "Unable to run VA RPC server")
}
app.Run()

View File

@ -19,9 +19,10 @@ import (
"github.com/letsencrypt/boulder/wfe"
)
func setupWFE(c cmd.Config) (rpc.RegistrationAuthorityClient, rpc.StorageAuthorityClient, chan *amqp.Error) {
ch, err := cmd.AmqpChannel(c)
func setupWFE(c cmd.Config, logger *blog.AuditLogger) (rpc.RegistrationAuthorityClient, rpc.StorageAuthorityClient, chan *amqp.Error) {
ch, err := rpc.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
logger.Info(" [!] Connected to AMQP")
closeChan := ch.NotifyClose(make(chan *amqp.Error, 1))
@ -55,14 +56,25 @@ func main() {
blog.SetAuditLogger(auditlogger)
go cmd.DebugServer(c.WFE.DebugAddr)
wfe, err := wfe.NewWebFrontEndImpl()
cmd.FailOnError(err, "Unable to create WFE")
rac, sac, closeChan := setupWFE(c)
rac, sac, closeChan := setupWFE(c, auditlogger)
wfe.RA = &rac
wfe.SA = &sac
wfe.Stats = stats
wfe.SubscriberAgreementURL = c.SubscriberAgreementURL
wfe.CertCacheDuration, err = time.ParseDuration(c.WFE.CertCacheDuration)
cmd.FailOnError(err, "Couldn't parse certificate caching duration")
wfe.CertNoCacheExpirationWindow, err = time.ParseDuration(c.WFE.CertNoCacheExpirationWindow)
cmd.FailOnError(err, "Couldn't parse certificate expiration no-cache window")
wfe.IndexCacheDuration, err = time.ParseDuration(c.WFE.IndexCacheDuration)
cmd.FailOnError(err, "Couldn't parse index caching duration")
wfe.IssuerCacheDuration, err = time.ParseDuration(c.WFE.IssuerCacheDuration)
cmd.FailOnError(err, "Couldn't parse issuer caching duration")
wfe.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
@ -74,25 +86,25 @@ func main() {
// with new RA and SA rpc clients.
for {
for err := range closeChan {
auditlogger.Warning(fmt.Sprintf("AMQP Channel closed, will reconnect in 5 seconds: [%s]", err))
auditlogger.Warning(fmt.Sprintf(" [!] AMQP Channel closed, will reconnect in 5 seconds: [%s]", err))
time.Sleep(time.Second * 5)
rac, sac, closeChan = setupWFE(c)
rac, sac, closeChan = setupWFE(c, auditlogger)
wfe.RA = &rac
wfe.SA = &sac
auditlogger.Warning("Reconnected to AMQP")
}
}
}()
// Set up paths
wfe.BaseURL = c.Common.BaseURL
wfe.HandlePaths()
h, err := wfe.Handler()
cmd.FailOnError(err, "Problem setting up HTTP handlers")
auditlogger.Info(app.VersionString())
// Add HandlerTimer to output resp time + success/failure stats to statsd
auditlogger.Info(fmt.Sprintf("Server running, listening on %s...\n", c.WFE.ListenAddress))
err = http.ListenAndServe(c.WFE.ListenAddress, cmd.HandlerTimer(http.DefaultServeMux, stats, "WFE"))
err = http.ListenAndServe(c.WFE.ListenAddress, cmd.HandlerTimer(h, stats, "WFE"))
cmd.FailOnError(err, "Error starting HTTP server")
}

View File

@ -1,106 +0,0 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/ca"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/ra"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/va"
"github.com/letsencrypt/boulder/wfe"
)
func main() {
app := cmd.NewAppShell("boulder")
app.Action = func(c cmd.Config) {
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
// Set up logging
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
cmd.FailOnError(err, "Could not connect to Syslog")
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer auditlogger.AuditPanic()
blog.SetAuditLogger(auditlogger)
// Run StatsD profiling
go cmd.ProfileCmd("Monolith", stats)
// Create the components
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)
ra := ra.NewRegistrationAuthorityImpl()
va := va.NewValidationAuthorityImpl(c.CA.TestMode)
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")
ca, err := ca.NewCertificateAuthorityImpl(cadb, c.CA, c.Common.IssuerCert)
cmd.FailOnError(err, "Unable to create CA")
if c.SQL.CreateTables {
err = sa.CreateTablesIfNotExists()
cmd.FailOnError(err, "Failed to create SA tables")
err = cadb.CreateTablesIfNotExists()
cmd.FailOnError(err, "Failed to create CA tables")
}
// Wire them up
wfei.RA = &ra
wfei.SA = sa
wfei.Stats = stats
wfei.SubscriberAgreementURL = c.SubscriberAgreementURL
wfei.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
ra.CA = ca
ra.SA = sa
ra.VA = &va
ra.Stats = stats
va.RA = &ra
va.Stats = stats
ca.SA = sa
// Set up paths
ra.AuthzBase = c.Common.BaseURL + wfe.AuthzPath
wfei.BaseURL = c.Common.BaseURL
wfei.HandlePaths()
ra.MaxKeySize = c.Common.MaxKeySize
ca.MaxKeySize = c.Common.MaxKeySize
auditlogger.Info(app.VersionString())
fmt.Fprintf(os.Stderr, "Server running, listening on %s...\n", c.WFE.ListenAddress)
err = http.ListenAndServe(c.WFE.ListenAddress, cmd.HandlerTimer(http.DefaultServeMux, stats, "Monolith"))
cmd.FailOnError(err, "Error starting HTTP server")
}
app.Run()
}

View File

@ -0,0 +1,292 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"crypto/x509"
"fmt"
"io/ioutil"
"sort"
"strings"
"text/template"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/mail"
"github.com/letsencrypt/boulder/rpc"
"github.com/letsencrypt/boulder/sa"
)
type emailContent struct {
ExpirationDate time.Time
DaysToExpiration int
DNSNames string
}
type regStore interface {
GetRegistration(int64) (core.Registration, error)
}
type mailer struct {
stats statsd.Statter
log *blog.AuditLogger
dbMap *gorp.DbMap
rs regStore
mailer mail.Mailer
emailTemplate *template.Template
nagTimes []time.Duration
limit int
}
func (m *mailer) sendNags(parsedCert *x509.Certificate, contacts []*core.AcmeURL) error {
expiresIn := int(parsedCert.NotAfter.Sub(time.Now()).Hours()/24) + 1
emails := []string{}
for _, contact := range contacts {
if contact.Scheme == "mailto" {
emails = append(emails, contact.Opaque)
}
}
if len(emails) > 0 {
email := emailContent{
ExpirationDate: parsedCert.NotAfter,
DaysToExpiration: expiresIn,
DNSNames: strings.Join(parsedCert.DNSNames, ", "),
}
msgBuf := new(bytes.Buffer)
err := m.emailTemplate.Execute(msgBuf, email)
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.TemplateFailure", 1, 1.0)
return err
}
startSending := time.Now()
err = m.mailer.SendMail(emails, msgBuf.String())
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.SendFailure", 1, 1.0)
return err
}
m.stats.TimingDuration("Mailer.Expiration.Sending", time.Since(startSending), 1.0)
m.stats.Inc("Mailer.Expiration.Sent", int64(len(emails)), 1.0)
}
return nil
}
func (m *mailer) updateCertStatus(serial string) error {
// Update CertificateStatus object
tx, err := m.dbMap.Begin()
if err != nil {
m.log.Err(fmt.Sprintf("Error opening transaction for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
csObj, err := tx.Get(&core.CertificateStatus{}, serial)
if err != nil {
m.log.Err(fmt.Sprintf("Error fetching status for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
certStatus := csObj.(*core.CertificateStatus)
certStatus.LastExpirationNagSent = time.Now()
_, err = tx.Update(certStatus)
if err != nil {
m.log.Err(fmt.Sprintf("Error updating status for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
m.log.Err(fmt.Sprintf("Error commiting transaction for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
return nil
}
func (m *mailer) processCerts(certs []core.Certificate) {
m.log.Info(fmt.Sprintf("expiration-mailer: Found %d certificates, starting sending messages", len(certs)))
for _, cert := range certs {
reg, err := m.rs.GetRegistration(cert.RegistrationID)
if err != nil {
m.log.Err(fmt.Sprintf("Error fetching registration %d: %s", cert.RegistrationID, err))
m.stats.Inc("Mailer.Expiration.Errors.GetRegistration", 1, 1.0)
continue
}
parsedCert, err := x509.ParseCertificate(cert.DER)
if err != nil {
m.log.Err(fmt.Sprintf("Error parsing certificate %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.ParseCertificate", 1, 1.0)
continue
}
err = m.sendNags(parsedCert, reg.Contact)
if err != nil {
m.log.Err(fmt.Sprintf("Error sending nag emails: %s", err))
m.stats.Inc("Mailer.Expiration.Errors.SendingNags", 1, 1.0)
continue
}
err = m.updateCertStatus(cert.Serial)
if err != nil {
m.log.Err(fmt.Sprintf("Error updating certificate status for %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.UpdateCertificateStatus", 1, 1.0)
continue
}
}
m.log.Info("expiration-mailer: Finished sending messages")
return
}
func (m *mailer) findExpiringCertificates() error {
now := time.Now()
// E.g. m.NagTimes = [1, 3, 7, 14] days from expiration
for i, expiresIn := range m.nagTimes {
left := now
if i > 0 {
left = left.Add(m.nagTimes[i-1])
}
right := now.Add(expiresIn)
m.log.Info(fmt.Sprintf("expiration-mailer: Searching for certificates that expire between %s and %s", left, right))
var certs []core.Certificate
_, err := m.dbMap.Select(
&certs,
`SELECT cert.* FROM certificates AS cert
JOIN certificateStatus AS cs
ON cs.serial = cert.serial
AND cert.expires > :cutoffA
AND cert.expires < :cutoffB
AND cert.status != "revoked"
AND cs.lastExpirationNagSent <= :nagCutoff
ORDER BY cert.expires ASC
LIMIT :limit`,
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": time.Now().Add(-expiresIn),
"limit": m.limit,
},
)
if err != nil {
m.log.Err(fmt.Sprintf("expiration-mailer: Error loading certificates: %s", err))
return err // fatal
}
if len(certs) > 0 {
processingStarted := time.Now()
m.processCerts(certs)
m.stats.TimingDuration("Mailer.Expiration.ProcessingCertificates", time.Since(processingStarted), 1.0)
}
}
return nil
}
type durationSlice []time.Duration
func (ds durationSlice) Len() int {
return len(ds)
}
func (ds durationSlice) Less(a, b int) bool {
return ds[a] < ds[b]
}
func (ds durationSlice) Swap(a, b int) {
ds[a], ds[b] = ds[b], ds[a]
}
func main() {
app := cmd.NewAppShell("expiration-mailer")
app.App.Flags = append(app.App.Flags, cli.IntFlag{
Name: "cert_limit",
Value: 100,
EnvVar: "CERT_LIMIT",
Usage: "Count of certificates to process per expiration period",
})
app.Config = func(c *cli.Context, config cmd.Config) cmd.Config {
if c.GlobalInt("cert_limit") > 0 {
config.Mailer.CertLimit = c.GlobalInt("cert_limit")
}
return config
}
app.Action = func(c cmd.Config) {
// Set up logging
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
cmd.FailOnError(err, "Could not connect to Syslog")
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer auditlogger.AuditPanic()
blog.SetAuditLogger(auditlogger)
auditlogger.Info(app.VersionString())
go cmd.DebugServer(c.Mailer.DebugAddr)
// Configure DB
dbMap, err := sa.NewDbMap(c.Mailer.DBDriver, c.Mailer.DBConnect)
cmd.FailOnError(err, "Could not connect to database")
ch, err := rpc.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
saRPC, err := rpc.NewAmqpRPCClient("ExpirationMailer->SA", c.AMQP.SA.Server, ch)
cmd.FailOnError(err, "Unable to create RPC client")
sac, err := rpc.NewStorageAuthorityClient(saRPC)
cmd.FailOnError(err, "Failed to create SA client")
// Load email template
emailTmpl, err := ioutil.ReadFile(c.Mailer.EmailTemplate)
cmd.FailOnError(err, fmt.Sprintf("Could not read email template file [%s]", c.Mailer.EmailTemplate))
tmpl, err := template.New("expiry-email").Parse(string(emailTmpl))
cmd.FailOnError(err, "Could not parse email template")
mailClient := mail.New(c.Mailer.Server, c.Mailer.Port, c.Mailer.Username, c.Mailer.Password)
var nags durationSlice
for _, nagDuration := range c.Mailer.NagTimes {
dur, err := time.ParseDuration(nagDuration)
if err != nil {
auditlogger.Err(fmt.Sprintf("Failed to parse nag duration string [%s]: %s", nagDuration, err))
return
}
nags = append(nags, dur)
}
// Make sure durations are sorted in increasing order
sort.Sort(nags)
m := mailer{
stats: stats,
log: auditlogger,
dbMap: dbMap,
rs: sac,
mailer: &mailClient,
emailTemplate: tmpl,
nagTimes: nags,
limit: c.Mailer.CertLimit,
}
auditlogger.Info("expiration-mailer: Starting")
err = m.findExpiringCertificates()
cmd.FailOnError(err, "expiration-mailer has failed")
}
app.Run()
}

View File

@ -0,0 +1,276 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"testing"
"text/template"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
)
func bigIntFromB64(b64 string) *big.Int {
bytes, _ := base64.URLEncoding.DecodeString(b64)
x := big.NewInt(0)
x.SetBytes(bytes)
return x
}
func intFromB64(b64 string) int {
return int(bigIntFromB64(b64).Int64())
}
type mockMail struct {
Messages []string
}
func (m *mockMail) Clear() {
m.Messages = []string{}
}
func (m *mockMail) SendMail(to []string, msg string) (err error) {
for _ = range to {
m.Messages = append(m.Messages, msg)
}
return
}
type fakeRegStore struct {
RegById map[int64]core.Registration
}
func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) {
r, ok := f.RegById[id]
if !ok {
msg := fmt.Sprintf("no such registration %d", id)
return r, sa.NoSuchRegistrationError{Msg: msg}
}
return r, nil
}
func newFakeRegStore() fakeRegStore {
return fakeRegStore{RegById: make(map[int64]core.Registration)}
}
const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`
var jsonKeyA = []byte(`{
"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB"
}`)
var jsonKeyB = []byte(`{
"kty":"RSA",
"n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
"e":"AAEAAQ"
}`)
var log = mocks.UseMockLog()
func TestSendNags(t *testing.T) {
tmpl, err := template.New("expiry-email").Parse(testTmpl)
test.AssertNotError(t, err, "Couldn't parse test email template")
stats, _ := statsd.NewNoopClient(nil)
mc := mockMail{}
rs := newFakeRegStore()
m := mailer{
stats: stats,
mailer: &mc,
emailTemplate: tmpl,
rs: rs,
}
cert := &x509.Certificate{
Subject: pkix.Name{
CommonName: "happy",
},
NotAfter: time.Now().AddDate(0, 0, 2),
DNSNames: []string{"example.com"},
}
email, _ := core.ParseAcmeURL("mailto:rolandshoemaker@gmail.com")
emailB, _ := core.ParseAcmeURL("mailto:test@gmail.com")
err = m.sendNags(cert, []*core.AcmeURL{email})
test.AssertNotError(t, err, "Failed to send warning messages")
test.AssertEquals(t, len(mc.Messages), 1)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[0])
mc.Clear()
err = m.sendNags(cert, []*core.AcmeURL{email, emailB})
test.AssertNotError(t, err, "Failed to send warning messages")
test.AssertEquals(t, len(mc.Messages), 2)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[1])
mc.Clear()
err = m.sendNags(cert, []*core.AcmeURL{})
test.AssertNotError(t, err, "Not an error to pass no email contacts")
test.AssertEquals(t, len(mc.Messages), 0)
}
var n = bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==")
var e = intFromB64("AQAB")
var d = bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==")
var p = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
var q = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
var testKey = rsa.PrivateKey{
PublicKey: rsa.PublicKey{N: n, E: e},
D: d,
Primes: []*big.Int{p, q},
}
func TestFindExpiringCertificates(t *testing.T) {
dbMap, err := sa.NewDbMap("sqlite3", ":memory:")
test.AssertNotError(t, err, "Couldn't connect to SQLite")
err = dbMap.CreateTablesIfNotExists()
test.AssertNotError(t, err, "Couldn't create tables")
tmpl, err := template.New("expiry-email").Parse(testTmpl)
test.AssertNotError(t, err, "Couldn't parse test email template")
stats, _ := statsd.NewNoopClient(nil)
mc := mockMail{}
rs := newFakeRegStore()
m := mailer{
log: blog.GetAuditLogger(),
stats: stats,
mailer: &mc,
emailTemplate: tmpl,
dbMap: dbMap,
rs: rs,
nagTimes: []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7},
limit: 100,
}
log.Clear()
err = m.findExpiringCertificates()
test.AssertNotError(t, err, "Failed on no certificates")
test.AssertEquals(t, len(log.GetAllMatching("Searching for certificates that expire between.*")), 3)
// Add some expiring certificates and registrations
emailA, _ := core.ParseAcmeURL("mailto:one@mail.com")
emailB, _ := core.ParseAcmeURL("mailto:twp@mail.com")
var keyA jose.JsonWebKey
var keyB jose.JsonWebKey
err = json.Unmarshal(jsonKeyA, &keyA)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
err = json.Unmarshal(jsonKeyB, &keyB)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
regA := core.Registration{
ID: 1,
Contact: []*core.AcmeURL{
emailA,
},
Key: keyA,
}
regB := core.Registration{
ID: 2,
Contact: []*core.AcmeURL{
emailB,
},
Key: keyB,
}
rawCertA := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy A",
},
NotAfter: time.Now().AddDate(0, 0, 1),
DNSNames: []string{"example-a.com"},
SerialNumber: big.NewInt(1337),
}
certDerA, _ := x509.CreateCertificate(rand.Reader, &rawCertA, &rawCertA, &testKey.PublicKey, &testKey)
certA := &core.Certificate{
RegistrationID: 1,
Status: core.StatusValid,
Serial: "001",
Expires: time.Now().AddDate(0, 0, 1),
DER: certDerA,
}
// Already sent a nag but too long ago
certStatusA := &core.CertificateStatus{Serial: "001", LastExpirationNagSent: time.Now().Add(-time.Hour * 24 * 3)}
rawCertB := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy B",
},
NotAfter: time.Now().AddDate(0, 0, 3),
DNSNames: []string{"example-b.com"},
SerialNumber: big.NewInt(1337),
}
certDerB, _ := x509.CreateCertificate(rand.Reader, &rawCertB, &rawCertB, &testKey.PublicKey, &testKey)
certB := &core.Certificate{
RegistrationID: 1,
Status: core.StatusValid,
Serial: "002",
Expires: time.Now().AddDate(0, 0, 3),
DER: certDerB,
}
// Already sent a nag for this period
certStatusB := &core.CertificateStatus{Serial: "002", LastExpirationNagSent: time.Now().Add(-time.Hour * 24 * 3)}
rawCertC := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy C",
},
NotAfter: time.Now().AddDate(0, 0, 7),
DNSNames: []string{"example-c.com"},
SerialNumber: big.NewInt(1337),
}
certDerC, _ := x509.CreateCertificate(rand.Reader, &rawCertC, &rawCertC, &testKey.PublicKey, &testKey)
certC := &core.Certificate{
RegistrationID: 2,
Status: core.StatusValid,
Serial: "003",
Expires: time.Now().AddDate(0, 0, 7),
DER: certDerC,
}
certStatusC := &core.CertificateStatus{Serial: "003"}
rs.RegById[regA.ID] = regA
rs.RegById[regB.ID] = regB
err = dbMap.Insert(certA)
test.AssertNotError(t, err, "Couldn't add certA")
err = dbMap.Insert(certB)
test.AssertNotError(t, err, "Couldn't add certB")
err = dbMap.Insert(certC)
test.AssertNotError(t, err, "Couldn't add certC")
err = dbMap.Insert(certStatusA)
test.AssertNotError(t, err, "Couldn't add certStatusA")
err = dbMap.Insert(certStatusB)
test.AssertNotError(t, err, "Couldn't add certStatusB")
err = dbMap.Insert(certStatusC)
test.AssertNotError(t, err, "Couldn't add certStatusC")
log.Clear()
err = m.findExpiringCertificates()
test.AssertNotError(t, err, "Failed to find expiring certs")
// Should get 001 and 003
test.AssertEquals(t, len(mc.Messages), 2)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-a.com is going to expire in 1 days (%s)`, rawCertA.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-c.com is going to expire in 7 days (%s)`, rawCertC.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), mc.Messages[1])
// A consecutive run shouldn't find anything
mc.Clear()
log.Clear()
err = m.findExpiringCertificates()
test.AssertNotError(t, err, "Failed to find expiring certs")
test.AssertEquals(t, len(mc.Messages), 0)
}

View File

@ -0,0 +1,74 @@
# Format of Certificate Data for Import by Let's Encrypt
There are three CSV files:
* A `domains` CSV file, which maps the SHA1 fingerprint of a certificate to the domain which the certificate applies to (as read from the Subject field and the Subject Alternative Name field of the given certificate). Note that any given domain may appear in multiple rows in this file (if there are multiple certificates for this domain). Similarly, any given fingerprint may appear in multiple rows (if a certificate applies to multiple domains, i.e. via the SAN field). However, the combination of domain and fingerprint constitutes a unique entry, and no two rows should have the same domain *and* fingerprint.
* A `valid-certs` CSV file, which contains all the details (other than the domain) of a given certificate. Each row in the table represents one certificate (but as mentioned above, may map to multiple domains via the `domains` file).
* An `invalid-certs` CSV file, which is identical in format to the `valid-certs` CSV file, but whose rows represent certificates that are no longer valid (due to expiration, revocation, etc.).
For the purposes of importing by the Let's Encrypt system, these tables will be provided as CSV files. Each entry in the CSV file will be enclosed by double quotes (") and interior double quotes will be backslash-escaped (\"). The columns will be as follows (in the order listed).
## Table Formats
###`domains`
1. Column Name: SHA1 Fingerprint
Data Type: 40 hexadecimal characters
Description: The SHA1 fingerprint of the DER-encoded certificate, in hexadecimal, without colons separating bytes.
Example: `"10A9C1F8ADAACBFE2B0F83F7D5FA1FC293A8D2A2"`
1. Column Name: Domain
Data Type: Up to 255 characters
Description: The domain to which the certificate applies, with the DNS labels reversed. Wildcards are included.
Example: `"org.eff.*"`
###`valid-certs`
1. Column Name: SHA1 Fingerprint
Data Type: 40 hexadecimal characters
Description: The SHA1 fingerprint of the DER-encoded certificate, in hexadecimal, without colons separating bytes. Matches the SHA1 Fingerprint column in the `domains_to_fingerprints` table.
Example: `"10A9C1F8ADAACBFE2B0F83F7D5FA1FC293A8D2A2"`
1. Column Name: Issuer
Data Type: Text
Description: The Issuer field for the certificate this row represents.
Example: `"C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Class 2 Primary Intermediate Server CA"`
1. Column Name: Subject
Data Type: Text
Description: The Subject field for the certificate this row represents.
Example: `"description=571208-SLe257oHY9fVQ07Z, C=US, ST=California, L=San Francisco, O=Electronic Frontier Foundation, Inc., CN=*.eff.org/emailAddress=hostmaster@eff.org"`
1. Column Name: Not Valid After Datetime
Data Type: A MySQL `DATETIME`, 19 characters long. (More information is available at https://dev.mysql.com/doc/refman/5.5/en/datetime.html)
Description: The UTC expiration date/time (not valid after date/time) for the certificate this row represents.
Example: `"2016-04-14 23:42:01"`
1. Column Name: Modulus (Public Key)
Data Type: Hexadecimal characters
Description: The public key for the certificate this row represents (hexadecimal characters only, no colons).
Example: `"EA402791CB7E2721CAC9EB916BC2FFA5C3D3AEB9EA1B0A76AAE8594DACC091AA9E3942B89165DEF25C081380E4F963AC6FF84DC2433BC8C15D2FD618C23AC9CD1A6DEB5A069B275E4A9F0E4840B9C6ED9F82715472575EF966648ADFB5BA7491E2A2D1C4DA74769D84537E42BC8664C413F84AE2451A4564B1817930914E0EFBB19BA76512A29F2A5E72B6C96B8AFD74CBEE6072E7969836540BECD286A1295DBE91803DB6AE87A193320E8787E18D4473D37FB153D1E0299CEFC7BC9E6CC2E1790B3516867B549EB30A5ECE36B715D3C949E3DFA33DD6A8D351898611459259BA5E25C8CB5CFBB2868C39FD1467C5096497690B962243E863D0391CFBCDAE99"`
1. Column Name: Valid?
Data Type: 0 or 1
Description: 0 if the certificate is no longer valid (due to expiration or revocation), and 1 if the certificate is still valid (as determined via the method described below in the section *Certificate Validity*).
Example: 1
1. Column Name: EV?
Data Type: 0 or 1
Description: 1 if the certificate is a valid Extended Validation (EV) certificate (as determined via the method described below in the section *EV Validity*), and 0 otherwise.
Example: 0
1. Column Name: Hex-Encoded Certificate
Data Type: Hexadecimal characters
Description: The hexadecimal encoding of the DER-encoded certificate.
Example:
`"308205653082044DA003020102020727D9C3047132A9300D06092A864886F70D01010505003081CA310B30090603550406130255533110300E060355040813074172697A6F6E61311330110603550407130A53636F74747364616C65311A3018060355040A1311476F44616464792E636F6D2C20496E632E31333031060355040B132A687474703A2F2F6365727469666963617465732E676F64616464792E636F6D2F7265706F7369746F72793130302E06035504031327476F204461646479205365637572652043657274696669636174696F6E20417574686F726974793111300F060355040513083037393639323837301E170D3132313233313139323434365A170D3136303230363139323132375A305931193017060355040A0C102A2E6C617965727661756C742E636F6D3121301F060355040B1318446F6D61696E20436F6E74726F6C2056616C6964617465643119301706035504030C102A2E6C617965727661756C742E636F6D30820122300D06092A864886F70D01010105000382010F003082010A0282010100A59FD2DF4BAAD4A968E1BCEBDAB01CA11296972458F23E9B411A32709CA71A72514E26DB997CA7DB8E4C4E5799A8F0D7FA67116D146DA0CCD4A21560382602D670033AF80C00AA972649B7703D5534AF46E4D4E4259DDCB7447C7F23BA131ACC099B1880DF92981FF0614DAA79DFEFBD3978F04FCB118FC9624D35889BEAC447E4999668F27EE85ADB144472168256E8DCF7E0A185D346D0795C06B1340D122AB3C0C717EBFD96642F72A05345143BA502D034CF0DAA47D8A62C56B05C7F80923386546463C491DD9C916C2885B78491CF035E3400D1BCD1F43D8A8D5CE7BEB79D05E67FC6834E60D3A624F6ACF14304EBBDF1E5E9E785EA70D691990AF46B9D0203010001A38201BE308201BA300F0603551D130101FF04053003010100301D0603551D250416301406082B0601050507030106082B06010505070302300E0603551D0F0101FF0404030205A030330603551D1F042C302A3028A026A0248622687474703A2F2F63726C2E676F64616464792E636F6D2F676473312D38322E63726C30530603551D20044C304A3048060B6086480186FD6D010717013039303706082B06010505070201162B687474703A2F2F6365727469666963617465732E676F64616464792E636F6D2F7265706F7369746F72792F30818006082B0601050507010104743072302406082B060105050730018618687474703A2F2F6F6373702E676F64616464792E636F6D2F304A06082B06010505073002863E687474703A2F2F6365727469666963617465732E676F64616464792E636F6D2F7265706F7369746F72792F67645F696E7465726D6564696174652E637274301F0603551D23041830168014FDAC6132936C45D6E2EE855F9ABAE7769968CCE7302B0603551D110424302282102A2E6C617965727661756C742E636F6D820E6C617965727661756C742E636F6D301D0603551D0E04160414355D68525E6F2915C2551C9915B0ED75596EAE86300D06092A864886F70D0101050500038201010087F6A332EB0EBB039209E74A529892B64F3FEDB00C16CA621928E9CC2862E9C6C2ABC120E3A6010157186D4B96D4B7B36C922C7193DDADB72E02CB3BAD4B17157452D2D2895B1CA969A9E29A36507540C317E8C68BE17EC5061A2AAEBE58E1E2DDA7B352484142D3D91D60D5779C6A4EA464269304471CBB0B025B1BBAAE4C0AF5A1DDB5D48D0697C5EF94A06B723DA1188CDA1DC1540CEC7A7D7604F8482F23B8716D6A98BCB7582170D9ED7D09F8993D865D2236DDC9075C3551CE810124796F909ECDF8FF04155AF0BDED3144B2E21C5561082D107F6720C5FFCB2CD003BA9CA4EF00E2A16B3F7F3D379D1180DFC1A6BBBAA195ACC8768BEBAF9CAB3A04BE"`
###`invalid-certs`
Same format as the `valid-certs` CSV file.
## Certificate Validity
A certificate is considered valid if the following `openssl` command reports the certificate is valid:
openssl verify -CApath root_cas -crl_check -untrusted intermediate_ca_certs certificate.pem
where
* `root_cas` is a directory containing the transitive closure of all valid CA certificates contained in the data source, starting with the union of the root CA certificates included with Mozilla Firefox and Microsoft Windows,
* `intermediate_ca_certs` is a file containing all the intermediate CAs the data source has seen in chains connected to the certificate in question, and,
* `certificate.pem` is the certificate in question in PEM format.
## EV Validity
A certificate is considered a valid Extended Validation certificate if it is valid as described in *Certificate Validity* and all of the following are true:
* An OID is given in the *Certificate Policies* X.509 extension field,
* The root certificate in one of the valid chains that end with this certificate is also in Mozilla's list of EV CAs (which can be found at https://mxr.mozilla.org/mozilla-central/source/security/certverifier/ExtendedValidation.cpp), and
* The OID associated with that root certificate and the OID given by the certificate in question are identical.

View File

@ -0,0 +1,181 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"encoding/csv"
"encoding/hex"
"fmt"
"io"
"math"
"os"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/sa"
)
const datestampFormat string = "2006-01-02 15:04:05"
func addCerts(csvFilename string, dbMap *gorp.DbMap, stats statsd.Statter, statsRate float32) {
file, err := os.Open(csvFilename)
cmd.FailOnError(err, "Could not open the file for reading")
csvReader := csv.NewReader(file)
for {
record, err := csvReader.Read()
if err == io.EOF {
break
} else if err != nil {
fmt.Println("Error:", err)
return
}
notAfter, err := time.Parse(datestampFormat, record[3])
spkiBytes, err := hex.DecodeString(record[4])
certDER, err := hex.DecodeString(record[7])
externalCert := core.ExternalCert{
SHA1: record[0],
Issuer: record[1],
Subject: record[2],
NotAfter: notAfter,
SPKI: spkiBytes,
Valid: record[5] == "1",
EV: record[6] == "1",
CertDER: certDER,
}
importStart := time.Now()
err = dbMap.Insert(&externalCert)
stats.TimingDuration("ExistingCert.CertImportTime", time.Since(importStart), statsRate)
stats.Inc("ExistingCert.CertsImported", 1, statsRate)
}
}
func addIdentifiers(csvFilename string, dbMap *gorp.DbMap, stats statsd.Statter, statsRate float32) {
file, err := os.Open(csvFilename)
cmd.FailOnError(err, "Could not open the file for reading")
csvReader := csv.NewReader(file)
for {
record, err := csvReader.Read()
if err == io.EOF {
break
} else if err != nil {
fmt.Println("Error:", err)
return
}
identifierData := core.IdentifierData{
ReversedName: record[1],
CertSHA1: record[0],
}
importStart := time.Now()
err = dbMap.Insert(&identifierData)
stats.TimingDuration("ExistingCert.DomainImportTime", time.Since(importStart), statsRate)
stats.Inc("ExistingCert.DomainsImported", 1, statsRate)
}
}
func removeInvalidCerts(csvFilename string, dbMap *gorp.DbMap, stats statsd.Statter, statsRate float32) {
file, err := os.Open(csvFilename)
cmd.FailOnError(err, "Could not open the file for reading")
csvReader := csv.NewReader(file)
for {
record, err := csvReader.Read()
if err == io.EOF {
break
} else if err != nil {
fmt.Println("Error:", err)
return
}
identifierData := core.IdentifierData{
CertSHA1: record[0],
}
externalCert := core.ExternalCert{
SHA1: record[0],
}
deleteStart := time.Now()
_, err = dbMap.Delete(&identifierData)
stats.TimingDuration("ExistingCert.DomainDeleteTime", time.Since(deleteStart), statsRate)
_, err = dbMap.Delete(&externalCert)
stats.TimingDuration("ExistingCert.CertDeleteTime", time.Since(deleteStart), statsRate)
stats.Inc("ExistingCert.CertsDeleted", 1, statsRate)
}
}
func main() {
app := cmd.NewAppShell("external-cert-importer")
app.App.Flags = append(app.App.Flags, cli.StringFlag{
Name: "a, valid-certs-file",
Value: "ssl-observatory-valid-certs.csv",
Usage: "The CSV file containing the valid certs to import.",
}, cli.StringFlag{
Name: "d, domains-file",
Value: "ssl-observatory-domains.csv",
Usage: "The CSV file containing the domains associated with the certs that are being imported.",
}, cli.StringFlag{
Name: "r, invalid-certs-file",
Value: "ssl-observatory-invalid-certs.csv",
Usage: "The CSV file Containing now invalid certs which should be removed.",
}, cli.Float64Flag{
Name: "statsd-rate",
Value: 0.1,
Usage: "A floating point number between 0 and 1 representing the rate at which the statsd client will send data.",
})
app.Config = func(c *cli.Context, config cmd.Config) cmd.Config {
fmt.Println(c.Args())
config.ExternalCertImporter.CertsToImportCSVFilename = c.GlobalString("a")
config.ExternalCertImporter.DomainsToImportCSVFilename = c.GlobalString("d")
config.ExternalCertImporter.CertsToRemoveCSVFilename = c.GlobalString("r")
config.ExternalCertImporter.StatsdRate = float32(math.Min(math.Max(c.Float64("statsd-rate"), 0.0), 1.0))
return config
}
app.Action = func(c cmd.Config) {
// Set up logging
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
cmd.FailOnError(err, "Could not connect to Syslog")
blog.SetAuditLogger(auditlogger)
// Configure DB
dbMap, err := sa.NewDbMap(c.Common.PolicyDB.Driver, c.Common.PolicyDB.Name)
cmd.FailOnError(err, "Could not connect to database")
dbMap.AddTableWithName(core.ExternalCert{}, "externalCerts").SetKeys(false, "SHA1")
dbMap.AddTableWithName(core.IdentifierData{}, "identifierData").SetKeys(false, "CertSHA1")
err = dbMap.CreateTablesIfNotExists()
cmd.FailOnError(err, "Could not create the tables")
// Note that this order of operations is intentional: we first add
// new certs to the database. Then, since certs are identified by
// the entries in the identifiers table, we add those. Then, we
// can remove invalid certs (which first removes the associated
// identifiers).
addCerts(c.ExternalCertImporter.CertsToImportCSVFilename, dbMap, stats, c.ExternalCertImporter.StatsdRate)
addIdentifiers(c.ExternalCertImporter.DomainsToImportCSVFilename, dbMap, stats, c.ExternalCertImporter.StatsdRate)
removeInvalidCerts(c.ExternalCertImporter.CertsToRemoveCSVFilename, dbMap, stats, c.ExternalCertImporter.StatsdRate)
}
app.Run()
}

View File

@ -1,94 +0,0 @@
package main
import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"time"
// "github.com/cloudflare/cfssl/crypto/pkcs11key"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/log"
)
var certFile = flag.String("ca", "", "JSON file for subject and validity")
var listFile = flag.String("revoked", "", "JSON file with a list of pkix.RevokedCertificate objects")
var module = flag.String("pkcs11-module", "", "PKCS#11 module")
var pin = flag.String("pkcs11-pin", "", "PKCS#11 password")
var token = flag.String("pkcs11-token", "", "PKCS#11 token name")
var label = flag.String("pkcs11-label", "", "PKCS#11 key label")
// Config defines the configuration loaded from listFile.
type Config struct {
ThisUpdate time.Time
NextUpdate time.Time
RevokedCerts []pkix.RevokedCertificate
}
func main() {
// Validate input
// All flags are required
flag.Parse()
missing := false
flag.VisitAll(func(f *flag.Flag) {
if len(f.Value.String()) == 0 {
missing = true
}
})
if missing {
log.Critical("All flags must be provided.")
flag.Usage()
return
}
// Read the issuer cert
certPEM, err := ioutil.ReadFile(*certFile)
if err != nil {
log.Criticalf("Unable to read certificate: %v", err)
return
}
certBlock, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
log.Criticalf("Unable to parse certificate: %v", err)
return
}
// Read the list of revoked certs
jsonConfig, err := ioutil.ReadFile(*listFile)
if err != nil {
log.Criticalf("Unable to read list of revoked certs: %v", err)
return
}
var config Config
err = json.Unmarshal(jsonConfig, &config)
if err != nil {
log.Criticalf("Unable to parse list of revoked certs: %v", err)
return
}
// Set up PKCS#11 key
priv, err := pkcs11key.New(*module, *token, *pin, *label)
if err != nil {
log.Criticalf("Unable to instantiate PKCS#11 private key: %v", err)
return
}
// Sign the CRL
crlDER, err := cert.CreateCRL(rand.Reader, priv, config.RevokedCerts, config.ThisUpdate, config.NextUpdate)
if err != nil {
log.Criticalf("Error signing certificate: %v", err)
return
}
fmt.Println(string(pem.EncodeToMemory(&pem.Block{
Type: "X509 CRL",
Bytes: crlDER,
})))
}

View File

@ -1,133 +0,0 @@
package main
import (
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"math/big"
"time"
// "github.com/cloudflare/cfssl/crypto/pkcs11key"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/log"
)
var configFile = flag.String("config", "", "JSON file for subject and validity")
var module = flag.String("pkcs11-module", "", "PKCS#11 module")
var pin = flag.String("pkcs11-pin", "", "PKCS#11 password")
var token = flag.String("pkcs11-token", "", "PKCS#11 token name")
var label = flag.String("pkcs11-label", "", "PKCS#11 key label")
// Config defines the configuration loaded from configFile.
type Config struct {
Name struct {
C string
O string
OU string
CN string
}
NotBefore time.Time
NotAfter time.Time
}
func main() {
// Validate input
// All flags are required
flag.Parse()
missing := false
flag.VisitAll(func(f *flag.Flag) {
if len(f.Value.String()) == 0 {
missing = true
}
})
if missing {
log.Critical("All flags must be provided.")
flag.Usage()
return
}
jsonConfig, err := ioutil.ReadFile(*configFile)
if err != nil {
log.Criticalf("Unable to read config: %v", err)
return
}
var config Config
err = json.Unmarshal(jsonConfig, &config)
if err != nil {
log.Criticalf("Unable to parse config: %v", err)
return
}
if len(config.Name.C) == 0 || len(config.Name.O) == 0 ||
len(config.Name.CN) == 0 {
log.Criticalf("Config must provide country, organizationName, and commonName")
return
}
if config.NotBefore.After(config.NotAfter) {
log.Criticalf("Invalid validity: notAfter is before notBefore")
return
}
// Set up PKCS#11 key
priv, err := pkcs11key.New(*module, *token, *pin, *label)
if err != nil {
log.Criticalf("Unable to instantiate PKCS#11 private key: %v", err)
return
}
pub := priv.Public()
// Generate serial number
serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialLimit)
if err != nil {
log.Criticalf("Error generating serial number: %v", err)
return
}
// Generate subject key ID
pubDER, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
log.Criticalf("Error serializing public key: %v", err)
return
}
h := sha1.New()
h.Write(pubDER)
keyID := h.Sum(nil)
// Sign the certificate
rootTemplate := &x509.Certificate{
SignatureAlgorithm: x509.SHA256WithRSA,
SerialNumber: serialNumber,
Subject: pkix.Name{
Country: []string{config.Name.C},
Organization: []string{config.Name.O},
CommonName: config.Name.CN,
},
NotBefore: config.NotBefore,
NotAfter: config.NotAfter,
BasicConstraintsValid: true,
IsCA: true,
SubjectKeyId: keyID,
}
rootDER, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, pub, priv)
if err != nil {
log.Criticalf("Error signing certificate: %v", err)
return
}
fmt.Println(string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: rootDER,
})))
}

View File

@ -65,7 +65,7 @@ func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool)
log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString))
var ocspResponse core.OCSPResponse
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
map[string]interface{}{"serial": serialString})
if err != nil {
present = false
@ -94,12 +94,14 @@ func main() {
blog.SetAuditLogger(auditlogger)
go cmd.DebugServer(c.OCSPResponder.DebugAddr)
go cmd.ProfileCmd("OCSP", stats)
auditlogger.Info(app.VersionString())
// Configure DB
dbMap, err := sa.NewDbMap(c.OCSPResponder.DBDriver, c.OCSPResponder.DBName)
dbMap, err := sa.NewDbMap(c.OCSPResponder.DBDriver, c.OCSPResponder.DBConnect)
cmd.FailOnError(err, "Could not connect to database")
sa.SetSQLDebug(dbMap, c.SQL.SQLDebug)
@ -115,11 +117,12 @@ func main() {
cmd.FailOnError(err, "Could not connect to OCSP database")
// Configure HTTP
http.Handle(c.OCSPResponder.Path, cfocsp.Responder{Source: src})
m := http.NewServeMux()
m.Handle(c.OCSPResponder.Path, cfocsp.Responder{Source: src})
// Add HandlerTimer to output resp time + success/failure stats to statsd
auditlogger.Info(fmt.Sprintf("Server running, listening on %s...\n", c.OCSPResponder.ListenAddress))
err = http.ListenAndServe(c.OCSPResponder.ListenAddress, cmd.HandlerTimer(http.DefaultServeMux, stats, "OCSP"))
err = http.ListenAndServe(c.OCSPResponder.ListenAddress, cmd.HandlerTimer(m, stats, "OCSP"))
cmd.FailOnError(err, "Error starting HTTP server")
}

View File

@ -9,7 +9,6 @@ import (
"crypto/x509"
"database/sql"
"fmt"
"math"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
@ -24,10 +23,21 @@ import (
"github.com/letsencrypt/boulder/sa"
)
const ocspResponseLimit int = 128
// FatalError indicates the updater should stop execution
type FatalError string
func (e FatalError) Error() string { return string(e) }
// OCSPUpdater contains the useful objects for the Updater
type OCSPUpdater struct {
stats statsd.Statter
log *blog.AuditLogger
cac rpc.CertificateAuthorityClient
dbMap *gorp.DbMap
}
func setupClients(c cmd.Config) (rpc.CertificateAuthorityClient, chan *amqp.Error) {
ch, err := cmd.AmqpChannel(c)
ch, err := rpc.AmqpChannel(c)
cmd.FailOnError(err, "Could not connect to AMQP")
closeChan := ch.NotifyClose(make(chan *amqp.Error, 1))
@ -41,7 +51,7 @@ func setupClients(c cmd.Config) (rpc.CertificateAuthorityClient, chan *amqp.Erro
return cac, closeChan
}
func processResponse(cac rpc.CertificateAuthorityClient, tx *gorp.Transaction, serial string) error {
func (updater *OCSPUpdater) processResponse(tx *gorp.Transaction, serial string) error {
certObj, err := tx.Get(core.Certificate{}, serial)
if err != nil {
return err
@ -72,7 +82,7 @@ func processResponse(cac rpc.CertificateAuthorityClient, tx *gorp.Transaction, s
RevokedAt: status.RevokedDate,
}
ocspResponse, err := cac.GenerateOCSP(signRequest)
ocspResponse, err := updater.cac.GenerateOCSP(signRequest)
if err != nil {
return err
}
@ -97,43 +107,77 @@ func processResponse(cac rpc.CertificateAuthorityClient, tx *gorp.Transaction, s
return nil
}
func findStaleResponses(cac rpc.CertificateAuthorityClient, dbMap *gorp.DbMap, oldestLastUpdatedTime time.Time, responseLimit int) error {
log := blog.GetAuditLogger()
// Produce one OCSP response for the given serial, returning err
// if anything went wrong. This method will open and commit a transaction.
func (updater *OCSPUpdater) updateOneSerial(serial string) error {
innerStart := time.Now()
// Each response gets a transaction. In the future we can increase
// performance by batching transactions.
// The key thing to think through is the cost of rollbacks, and whether
// we should rollback if CA/HSM fails to sign the response or only
// upon a partial DB insert.
tx, err := updater.dbMap.Begin()
if err != nil {
updater.log.Err(fmt.Sprintf("OCSP %s: Error starting transaction, aborting: %s", serial, err))
updater.stats.Inc("OCSP.UpdatesFailed", 1, 1.0)
tx.Rollback()
// Failure to begin transaction is a fatal error.
return FatalError(err.Error())
}
if err := updater.processResponse(tx, serial); err != nil {
updater.log.Err(fmt.Sprintf("OCSP %s: Could not process OCSP Response, skipping: %s", serial, err))
updater.stats.Inc("OCSP.UpdatesFailed", 1, 1.0)
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
updater.log.Err(fmt.Sprintf("OCSP %s: Error committing transaction, skipping: %s", serial, err))
updater.stats.Inc("OCSP.UpdatesFailed", 1, 1.0)
tx.Rollback()
return err
}
updater.log.Info(fmt.Sprintf("OCSP %s: OK", serial))
updater.stats.Inc("OCSP.UpdatesProcessed", 1, 1.0)
updater.stats.TimingDuration("OCSP.UpdateTime", time.Since(innerStart), 1.0)
return nil
}
// findStaleResponses opens a transaction and processes up to responseLimit
// responses in a single batch. The responseLimit should be relatively small,
// so as to limit the chance of the transaction failing due to concurrent
// updates.
func (updater *OCSPUpdater) findStaleResponses(oldestLastUpdatedTime time.Time, responseLimit int) error {
var certificateStatus []core.CertificateStatus
_, err := dbMap.Select(&certificateStatus,
_, err := updater.dbMap.Select(&certificateStatus,
`SELECT cs.* FROM certificateStatus AS cs JOIN certificates AS cert ON cs.serial = cert.serial
WHERE cs.ocspLastUpdated < ? AND cert.expires > now()
ORDER BY cs.ocspLastUpdated ASC
LIMIT ?`, oldestLastUpdatedTime, responseLimit)
if err == sql.ErrNoRows {
log.Info("All up to date. No OCSP responses needed.")
updater.log.Info("All up to date. No OCSP responses needed.")
} else if err != nil {
log.Err(fmt.Sprintf("Error loading certificate status: %s", err))
updater.log.Err(fmt.Sprintf("Error loading certificate status: %s", err))
} else {
log.Info(fmt.Sprintf("Processing OCSP Responses...\n"))
updater.log.Info(fmt.Sprintf("Processing OCSP Responses...\n"))
outerStart := time.Now()
for i, status := range certificateStatus {
log.Info(fmt.Sprintf("OCSP %d: %s", i, status.Serial))
updater.log.Debug(fmt.Sprintf("OCSP %s: (%d/%d)", status.Serial, i, responseLimit))
// Each response gets a transaction. To speed this up, we can batch
// transactions.
tx, err := dbMap.Begin()
if err != nil {
log.Err(fmt.Sprintf("Error starting transaction, aborting: %s", err))
tx.Rollback()
err = updater.updateOneSerial(status.Serial)
// Abort if we recieve a fatal error
if _, ok := err.(FatalError); ok {
return err
}
if err := processResponse(cac, tx, status.Serial); err != nil {
log.Err(fmt.Sprintf("Could not process OCSP Response for %s: %s", status.Serial, err))
tx.Rollback()
return err
}
log.Info(fmt.Sprintf("OCSP %d: %s OK", i, status.Serial))
tx.Commit()
}
updater.stats.TimingDuration("OCSP.BatchTime", time.Since(outerStart), 1.0)
updater.stats.Inc("OCSP.BatchesProcessed", 1, 1.0)
}
return err
@ -144,7 +188,7 @@ func main() {
app.App.Flags = append(app.App.Flags, cli.IntFlag{
Name: "limit",
Value: ocspResponseLimit,
Value: 100,
EnvVar: "OCSP_LIMIT",
Usage: "Count of responses to process per run",
})
@ -167,8 +211,10 @@ func main() {
blog.SetAuditLogger(auditlogger)
go cmd.DebugServer(c.OCSPUpdater.DebugAddr)
// Configure DB
dbMap, err := sa.NewDbMap(c.OCSPUpdater.DBDriver, c.OCSPUpdater.DBName)
dbMap, err := sa.NewDbMap(c.OCSPUpdater.DBDriver, c.OCSPUpdater.DBConnect)
cmd.FailOnError(err, "Could not connect to database")
cac, closeChan := setupClients(c)
@ -177,7 +223,7 @@ func main() {
// Abort if we disconnect from AMQP
for {
for err := range closeChan {
auditlogger.Warning(fmt.Sprintf("AMQP Channel closed, aborting early: [%s]", err))
auditlogger.Warning(fmt.Sprintf(" [!] AMQP Channel closed, aborting early: [%s]", err))
panic(err)
}
}
@ -185,6 +231,13 @@ func main() {
auditlogger.Info(app.VersionString())
updater := &OCSPUpdater{
cac: cac,
dbMap: dbMap,
stats: stats,
log: auditlogger,
}
// Calculate the cut-off timestamp
if c.OCSPUpdater.MinTimeToExpiry == "" {
panic("Config must specify a MinTimeToExpiry period.")
@ -195,9 +248,10 @@ func main() {
oldestLastUpdatedTime := time.Now().Add(-dur)
auditlogger.Info(fmt.Sprintf("Searching for OCSP responses older than %s", oldestLastUpdatedTime))
count := int(math.Min(float64(ocspResponseLimit), float64(c.OCSPUpdater.ResponseLimit)))
err = findStaleResponses(cac, dbMap, oldestLastUpdatedTime, count)
// When we choose to batch responses, it may be best to restrict count here,
// change the transaction to survive the whole findStaleResponses, and to
// loop this method call however many times is appropriate.
err = updater.findStaleResponses(oldestLastUpdatedTime, c.OCSPUpdater.ResponseLimit)
if err != nil {
auditlogger.WarningErr(err)
}

View File

@ -22,14 +22,15 @@
package cmd
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"strings"
@ -37,11 +38,8 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
"github.com/letsencrypt/boulder/ca"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/rpc"
)
// Config stores configuration parameters that applications
@ -50,33 +48,61 @@ import (
//
// Note: NO DEFAULTS are provided.
type Config struct {
ActivityMonitor struct {
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
// General
AMQP struct {
Server string
RA Queue
VA Queue
SA Queue
CA Queue
OCSP Queue
TLS *TLSConfig
Server string
Insecure bool
RA Queue
VA Queue
SA Queue
CA Queue
OCSP Queue
TLS *TLSConfig
}
WFE struct {
BaseURL string
ListenAddress string
CertCacheDuration string
CertNoCacheExpirationWindow string
IndexCacheDuration string
IssuerCacheDuration string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
CA ca.Config
Monolith struct {
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
RA struct {
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
SA struct {
DBDriver string
DBName string
DBDriver string
DBConnect string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
VA struct {
DNSResolver string
DNSTimeout string
UserAgent string
UserAgent string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
SQL struct {
@ -96,29 +122,53 @@ type Config struct {
}
Revoker struct {
DBDriver string
DBName string
DBDriver string
DBConnect string
}
Mail struct {
Mailer struct {
Server string
Port string
Username string
Password string
DBDriver string
DBConnect string
CertLimit int
NagTimes []string
// Path to a text/template email template
EmailTemplate string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
OCSPResponder struct {
DBDriver string
DBName string
DBConnect string
Path string
ListenAddress string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
OCSPUpdater struct {
DBDriver string
DBName string
DBConnect string
MinTimeToExpiry string
ResponseLimit int
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
ExternalCertImporter struct {
CertsToImportCSVFilename string
DomainsToImportCSVFilename string
CertsToRemoveCSVFilename string
StatsdRate float32
}
Common struct {
@ -126,6 +176,14 @@ type Config struct {
// Path to a PEM-encoded copy of the issuer certificate.
IssuerCert string
MaxKeySize int
DNSResolver string
DNSTimeout string
PolicyDB struct {
Driver string
Name string
}
}
SubscriberAgreementURL string
@ -201,98 +259,11 @@ func (as *AppShell) VersionString() string {
func FailOnError(err error, msg string) {
if err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
fmt.Fprintf(os.Stderr, "%s: %s", msg, err)
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(1)
}
}
// AmqpChannel is the same as amqpConnect in boulder, but with even
// 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 strings.HasPrefix(conf.AMQP.Server, "amqps") == false {
err = fmt.Errorf("AMQPS: TLS configuration provided, but not using an AMQPS URL")
return nil, err
}
cfg := new(tls.Config)
// 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
}
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.TLS.CACertFile)
if err != nil {
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()
}
// RunForever starts the server and wait around
func RunForever(server *rpc.AmqpRPCServer) {
forever := make(chan bool)
server.Start()
fmt.Fprintf(os.Stderr, "Server running...\n")
<-forever
}
// RunUntilSignaled starts the server and run until we get something on closeChan
func RunUntilSignaled(logger *blog.AuditLogger, server *rpc.AmqpRPCServer, closeChan chan *amqp.Error) {
server.Start()
fmt.Fprintf(os.Stderr, "Server running...\n")
// Block until channel closes
err := <-closeChan
logger.Warning(fmt.Sprintf("AMQP Channel closed, will reconnect in 5 seconds: [%s]", err))
time.Sleep(time.Second * 5)
logger.Warning("Reconnecting to AMQP...")
}
// ProfileCmd runs forever, sending Go runtime statistics to StatsD.
func ProfileCmd(profileName string, stats statsd.Statter) {
for {
@ -369,3 +340,15 @@ func HandlerTimer(handler http.Handler, stats statsd.Statter, prefix string) htt
stats.TimingDuration(fmt.Sprintf("HttpResponseTime.%s.%s", endpoint, state), cClosed, 1.0)
})
}
func DebugServer(addr string) {
if addr == "" {
log.Fatalf("unable to boot debug server because no address was given for it. Set debugAddr.")
}
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("unable to boot debug server on %#v", addr)
}
log.Printf("booting debug server at %#v", addr)
log.Println(http.Serve(ln, nil))
}

View File

@ -5,12 +5,6 @@
package core
import (
"crypto/rand"
"encoding/hex"
blog "github.com/letsencrypt/boulder/log"
)
// SimpleHTTPChallenge constructs a random HTTP challenge
func SimpleHTTPChallenge() Challenge {
tls := true
@ -24,20 +18,10 @@ func SimpleHTTPChallenge() Challenge {
// DvsniChallenge constructs a random DVSNI challenge
func DvsniChallenge() Challenge {
nonce := make([]byte, 16)
_, err := rand.Read(nonce)
if err != nil {
audit := blog.GetAuditLogger()
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
audit.EmergencyExit(err.Error())
}
return Challenge{
Type: ChallengeTypeDVSNI,
Status: StatusPending,
R: RandomString(32),
Nonce: hex.EncodeToString(nonce),
Token: NewToken(),
}
}

View File

@ -30,24 +30,13 @@ func TestChallenges(t *testing.T) {
if dvsni.Status != StatusPending {
t.Errorf("Incorrect status for challenge: %v", dvsni.Status)
}
if len(dvsni.R) != 43 {
t.Errorf("Incorrect length for DVSNI R: %v", dvsni.R)
}
if len(dvsni.Nonce) != 32 {
t.Errorf("Incorrect length for DVSNI nonce: %v", dvsni.Nonce)
}
}
// objects.go
var testCertificateRequestBadCSR = []byte(`{"csr":"AAAA"}`)
var testCertificateRequestGood = []byte(`{
"csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQWUlnRrm5ErSVkTzBTk3isg1hNydfyY4NM1P_N1S-ZeD39HMrYJsQkUh2tKvy3ztfmEqWpekvO4WRktSa000BPoAAwCgYIKoZIzj0EAwMDSQAwRgIhAIZIBwu4xOUD_4dJuGgceSKaoXTFBQKA3BFBNVJvbpdsAiEAlfq3Dq_8dnYbtmyDdXgopeKkSV5_76VSpcog-wkwEwo",
"authorizations": [
"https://example.com/authz/1",
"https://example.com/authz/2",
"https://example.com/authz/3"
]
"csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQWUlnRrm5ErSVkTzBTk3isg1hNydfyY4NM1P_N1S-ZeD39HMrYJsQkUh2tKvy3ztfmEqWpekvO4WRktSa000BPoAAwCgYIKoZIzj0EAwMDSQAwRgIhAIZIBwu4xOUD_4dJuGgceSKaoXTFBQKA3BFBNVJvbpdsAiEAlfq3Dq_8dnYbtmyDdXgopeKkSV5_76VSpcog-wkwEwo"
}`)
func TestCertificateRequest(t *testing.T) {
@ -61,9 +50,6 @@ func TestCertificateRequest(t *testing.T) {
if err = VerifyCSR(goodCR.CSR); err != nil {
t.Errorf("Valid CSR in CertificateRequest failed to verify: %v", err)
}
if len(goodCR.Authorizations) == 0 {
t.Errorf("Certificate request parsing failed to parse authorizations")
}
// Bad CSR
var badCR CertificateRequest
@ -84,34 +70,28 @@ func TestCertificateRequest(t *testing.T) {
}
func TestMergeChallenge(t *testing.T) {
tls := true
t1 := time.Now()
t2 := time.Now().Add(-5 * time.Hour)
challenge := Challenge{
Type: ChallengeTypeSimpleHTTP,
Status: StatusPending,
Validated: &t1,
Token: "asdf",
Path: "",
R: "asdf",
S: "",
Nonce: "asdf",
}
response := Challenge{
Type: ChallengeTypeSimpleHTTP,
Status: StatusValid,
Validated: &t2,
Token: "qwer",
Path: "qwer",
R: "qwer",
S: "qwer",
Nonce: "qwer",
TLS: &tls,
}
merged := Challenge{
Type: ChallengeTypeSimpleHTTP,
Status: StatusPending,
Validated: &t1,
Token: "asdf",
Path: "qwer",
R: "asdf",
S: "qwer",
Nonce: "asdf",
TLS: &tls,
}
probe := challenge.MergeResponse(response)
@ -124,17 +104,8 @@ func TestMergeChallenge(t *testing.T) {
if probe.Token != merged.Token {
t.Errorf("MergeChallenge allowed response to overwrite status")
}
if probe.Path != merged.Path {
t.Errorf("MergeChallenge failed to copy path from response")
}
if probe.R != merged.R {
t.Errorf("MergeChallenge allowed response to overwrite R")
}
if probe.Path != merged.Path {
t.Errorf("MergeChallenge failed to copy S from response")
}
if probe.Nonce != merged.Nonce {
t.Errorf("MergeChallenge allowed response to overwrite nonce")
if probe.TLS != merged.TLS {
t.Errorf("MergeChallenge failed to overwrite TLS")
}
}
@ -237,7 +208,7 @@ func TestURL(t *testing.T) {
jsonURL := fmt.Sprintf(`{"URL":"%s://%s%s?%s"}`, scheme, host, path, query)
badJSON := `{"URL":666}`
var url struct{ URL AcmeURL }
url := struct{ URL *AcmeURL }{URL: &AcmeURL{}}
err := json.Unmarshal([]byte(jsonURL), &url)
if err != nil {
t.Errorf("Error in json unmarshal: %v", err)
@ -260,7 +231,7 @@ func TestURL(t *testing.T) {
t.Errorf("Error in json marshal: %v", err)
}
if string(marshaledURL) != jsonURL {
t.Errorf("Improper marshaled URL: %s", string(marshaledURL))
t.Errorf("Expected marshaled url %#v, got %#v", jsonURL, string(marshaledURL))
}
}

View File

@ -6,7 +6,6 @@
package core
import (
"errors"
"fmt"
"math/rand"
"net"
@ -15,35 +14,34 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
// DNSSECError indicates an error caused by DNSSEC failing.
type DNSSECError struct {
}
// Error gives the DNSSEC failure notice.
func (err DNSSECError) Error() string {
return "DNSSEC validation failure"
}
// DNSResolver represents a resolver system
type DNSResolver struct {
// DNSResolverImpl represents a resolver system
type DNSResolverImpl struct {
DNSClient *dns.Client
Servers []string
}
// NewDNSResolver constructs a new DNS resolver object that utilizes the
// NewDNSResolverImpl constructs a new DNS resolver object that utilizes the
// provided list of DNS servers for resolution.
func NewDNSResolver(dialTimeout time.Duration, servers []string) *DNSResolver {
func NewDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolverImpl {
dnsClient := new(dns.Client)
// Set timeout for underlying net.Conn
dnsClient.DialTimeout = dialTimeout
return &DNSResolver{DNSClient: dnsClient, Servers: servers}
return &DNSResolverImpl{DNSClient: dnsClient, Servers: servers}
}
// ExchangeOne performs a single DNS exchange with a randomly chosen server
// out of the server list, returning the response, time, and error (if any)
func (dnsResolver *DNSResolver) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.Duration, err error) {
// out of the server list, returning the response, time, and error (if any).
// This method sets the DNSSEC OK bit on the message to true before sending
// it to the resolver in case validation isn't the resolvers default behaviour.
func (dnsResolver *DNSResolverImpl) ExchangeOne(hostname string, qtype uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
m := new(dns.Msg)
// Set question type
m.SetQuestion(dns.Fqdn(hostname), qtype)
// Set DNSSEC OK bit for resolver
m.SetEdns0(4096, true)
if len(dnsResolver.Servers) < 1 {
err = fmt.Errorf("Not configured with at least one DNS Server")
return
@ -55,60 +53,25 @@ func (dnsResolver *DNSResolver) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.
return dnsResolver.DNSClient.Exchange(m, chosenServer)
}
// LookupDNSSEC sends the provided DNS message to a randomly chosen server (see
// ExchangeOne) with DNSSEC enabled. If the lookup fails, this method sends a
// clarification query to determine if it's because DNSSEC was invalid or just
// a run-of-the-mill error. If it's because of DNSSEC, it returns ErrorDNSSEC.
func (dnsResolver *DNSResolver) LookupDNSSEC(m *dns.Msg) (*dns.Msg, time.Duration, error) {
// Set DNSSEC OK bit
m.SetEdns0(4096, true)
r, rtt, err := dnsResolver.ExchangeOne(m)
if err != nil {
return r, rtt, err
}
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
if r.Rcode == dns.RcodeServerFailure {
// Re-send query with +cd to see if SERVFAIL was caused by DNSSEC
// validation failure at the resolver
m.CheckingDisabled = true
checkR, checkRtt, err := dnsResolver.ExchangeOne(m)
if err != nil {
return r, checkRtt + rtt, err
}
if checkR.Rcode != dns.RcodeServerFailure {
// DNSSEC error, so we return the testable object.
err = DNSSECError{}
return r, checkRtt + rtt, err
}
}
err = fmt.Errorf("Invalid response code: %d-%s", r.Rcode, dns.RcodeToString[r.Rcode])
return r, rtt, err
}
return r, rtt, err
}
// LookupTXT uses a DNSSEC-enabled query to find all TXT records associated with
// the provided hostname. If the query fails due to DNSSEC, error will be
// set to ErrorDNSSEC.
func (dnsResolver *DNSResolver) LookupTXT(hostname string) ([]string, time.Duration, error) {
// LookupTXT sends a DNS query to find all TXT records associated with
// the provided hostname.
func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.Duration, error) {
var txt []string
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeTXT)
r, rtt, err := dnsResolver.LookupDNSSEC(m)
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeTXT)
if err != nil {
return nil, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for TXT query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, 0, err
}
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeTXT {
txtRec := answer.(*dns.TXT)
for _, field := range txtRec.Txt {
txt = append(txt, field)
if txtRec, ok := answer.(*dns.TXT); ok {
for _, field := range txtRec.Txt {
txt = append(txt, field)
}
}
}
}
@ -116,52 +79,65 @@ func (dnsResolver *DNSResolver) LookupTXT(hostname string) ([]string, time.Durat
return txt, rtt, err
}
// LookupHost uses a DNSSEC-enabled query to find all A/AAAA records associated with
// the provided hostname. If the query fails due to DNSSEC, error will be
// set to ErrorDNSSEC.
func (dnsResolver *DNSResolver) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
// LookupHost sends a DNS query to find all A/AAAA records associated with
// the provided hostname.
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
var addrs []net.IP
var answers []dns.RR
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeA)
r, aRtt, err := dnsResolver.LookupDNSSEC(m)
r, aRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeA)
if err != nil {
return addrs, 0, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for A query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt, 0, err
}
answers = append(answers, r.Answer...)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeAAAA)
r, aaaaRtt, err := dnsResolver.LookupDNSSEC(m)
r, aaaaRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeAAAA)
if err != nil {
return addrs, 0, 0, err
return addrs, aRtt, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for AAAA query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt, aaaaRtt, err
}
answers = append(answers, r.Answer...)
for _, answer := range answers {
if answer.Header().Rrtype == dns.TypeA {
a := answer.(*dns.A)
addrs = append(addrs, a.A)
if a, ok := answer.(*dns.A); ok {
addrs = append(addrs, a.A)
}
} else if answer.Header().Rrtype == dns.TypeAAAA {
aaaa := answer.(*dns.AAAA)
addrs = append(addrs, aaaa.AAAA)
if aaaa, ok := answer.(*dns.AAAA); ok {
addrs = append(addrs, aaaa.AAAA)
}
}
}
return addrs, aRtt, aaaaRtt, nil
}
// LookupCNAME uses a DNSSEC-enabled query to records for domain and returns either
// the target, "", or a if the query fails due to DNSSEC, error will be set to
// ErrorDNSSEC.
func (dnsResolver *DNSResolver) LookupCNAME(domain string) (string, time.Duration, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME)
r, rtt, err := dnsResolver.LookupDNSSEC(m)
// LookupCNAME returns the target name if a CNAME record exists for
// the given domain name. If the CNAME does not exist (NXDOMAIN,
// NXRRSET, or a successful response with no CNAME records), it
// returns the empty string and a nil error.
func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCNAME)
if err != nil {
return "", 0, err
}
if r.Rcode == dns.RcodeNXRrset || r.Rcode == dns.RcodeNameError {
return "", rtt, nil
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
return "", rtt, err
}
for _, answer := range r.Answer {
if cname, ok := answer.(*dns.CNAME); ok {
@ -172,29 +148,73 @@ func (dnsResolver *DNSResolver) LookupCNAME(domain string) (string, time.Duratio
return "", rtt, nil
}
// LookupCAA uses a DNSSEC-enabled query to find all CAA records associated with
// the provided hostname. If the query fails due to DNSSEC, error will be
// set to ErrorDNSSEC.
func (dnsResolver *DNSResolver) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeCAA)
// LookupDNAME is LookupCNAME, but for DNAME.
func (dnsResolver *DNSResolverImpl) LookupDNAME(hostname string) (string, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeDNAME)
if err != nil {
return "", 0, err
}
if r.Rcode == dns.RcodeNXRrset || r.Rcode == dns.RcodeNameError {
return "", rtt, nil
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for DNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
return "", rtt, err
}
r, rtt, err := dnsResolver.LookupDNSSEC(m)
for _, answer := range r.Answer {
if cname, ok := answer.(*dns.DNAME); ok {
return cname.Target, rtt, nil
}
}
return "", rtt, nil
}
// LookupCAA sends a DNS query to find all CAA records associated with
// the provided hostname. If the response code from the resolver is
// SERVFAIL an empty slice of CAA records is returned.
func (dnsResolver *DNSResolverImpl) LookupCAA(hostname string) ([]*dns.CAA, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCAA)
if err != nil {
return nil, 0, err
}
// On resolver validation failure, or other server failures, return empty an
// set and no error.
var CAAs []*dns.CAA
if r.Rcode == dns.RcodeServerFailure {
return CAAs, rtt, nil
}
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeCAA {
caaR, ok := answer.(*dns.CAA)
if !ok {
err = errors.New("Badly formatted record")
return nil, rtt, err
if caaR, ok := answer.(*dns.CAA); ok {
CAAs = append(CAAs, caaR)
}
CAAs = append(CAAs, caaR)
}
}
return CAAs, rtt, nil
}
// LookupMX sends a DNS query to find a MX record associated hostname and returns the
// record target.
func (dnsResolver *DNSResolverImpl) LookupMX(hostname string) ([]string, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeMX)
if err != nil {
return nil, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for MX query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, rtt, err
}
var results []string
for _, answer := range r.Answer {
if mx, ok := answer.(*dns.MX); ok {
results = append(results, mx.Mx)
}
}
return CAAs, rtt, nil
return results, rtt, nil
}

View File

@ -6,6 +6,10 @@
package core
import (
"fmt"
"net"
"os"
"strings"
"testing"
"time"
@ -14,37 +18,179 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
func TestDNSNoServers(t *testing.T) {
obj := NewDNSResolver(time.Hour, []string{})
const dnsLoopbackAddr = "127.0.0.1:4053"
func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
defer w.Close()
m := new(dns.Msg)
_, _, err := obj.ExchangeOne(m)
m.SetReply(r)
m.Compress = false
appendAnswer := func(rr dns.RR) {
m.Answer = append(m.Answer, rr)
}
for _, q := range r.Question {
q.Name = strings.ToLower(q.Name)
if q.Name == "servfail.com." {
m.Rcode = dns.RcodeServerFailure
break
}
switch q.Qtype {
case dns.TypeSOA:
record := new(dns.SOA)
record.Hdr = dns.RR_Header{Name: "letsencrypt.org.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 0}
record.Ns = "ns.letsencrypt.org."
record.Mbox = "master.letsencrypt.org."
record.Serial = 1
record.Refresh = 1
record.Retry = 1
record.Expire = 1
record.Minttl = 1
appendAnswer(record)
case dns.TypeA:
if q.Name == "cps.letsencrypt.org." {
record := new(dns.A)
record.Hdr = dns.RR_Header{Name: "cps.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
record.A = net.ParseIP("127.0.0.1")
appendAnswer(record)
}
case dns.TypeCNAME:
if q.Name == "cname.letsencrypt.org." {
record := new(dns.CNAME)
record.Hdr = dns.RR_Header{Name: "cname.letsencrypt.org.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 30}
record.Target = "cps.letsencrypt.org."
appendAnswer(record)
}
if q.Name == "cname.example.com." {
record := new(dns.CNAME)
record.Hdr = dns.RR_Header{Name: "cname.example.com.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 30}
record.Target = "CAA.example.com."
appendAnswer(record)
}
case dns.TypeDNAME:
if q.Name == "dname.letsencrypt.org." {
record := new(dns.DNAME)
record.Hdr = dns.RR_Header{Name: "dname.letsencrypt.org.", Rrtype: dns.TypeDNAME, Class: dns.ClassINET, Ttl: 30}
record.Target = "cps.letsencrypt.org."
appendAnswer(record)
}
case dns.TypeCAA:
if q.Name == "bracewel.net." || q.Name == "caa.example.com." {
record := new(dns.CAA)
record.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0}
record.Tag = "issue"
record.Value = "letsencrypt.org"
record.Flag = 1
appendAnswer(record)
}
if q.Name == "cname.example.com." {
record := new(dns.CAA)
record.Hdr = dns.RR_Header{Name: "caa.example.com.", Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0}
record.Tag = "issue"
record.Value = "letsencrypt.org"
record.Flag = 1
appendAnswer(record)
}
}
}
w.WriteMsg(m)
return
}
func serveLoopResolver(stopChan chan bool) chan bool {
dns.HandleFunc(".", mockDNSQuery)
server := &dns.Server{Addr: dnsLoopbackAddr, Net: "udp", ReadTimeout: time.Millisecond, WriteTimeout: time.Millisecond}
waitChan := make(chan bool, 1)
go func() {
waitChan <- true
err := server.ListenAndServe()
if err != nil {
fmt.Println(err)
return
}
}()
go func() {
<-stopChan
err := server.Shutdown()
if err != nil {
fmt.Println(err)
}
}()
return waitChan
}
func TestMain(m *testing.M) {
stop := make(chan bool, 1)
wait := serveLoopResolver(stop)
<-wait
ret := m.Run()
stop <- true
os.Exit(ret)
}
func TestDNSNoServers(t *testing.T) {
obj := NewDNSResolverImpl(time.Hour, []string{})
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeA)
test.AssertError(t, err, "No servers")
}
func TestDNSOneServer(t *testing.T) {
obj := NewDNSResolver(time.Second*10, []string{"8.8.8.8:53"})
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
m := new(dns.Msg)
m.SetQuestion("letsencrypt.org.", dns.TypeSOA)
_, _, err := obj.ExchangeOne(m)
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA)
test.AssertNotError(t, err, "No message")
}
func TestDNSDuplicateServers(t *testing.T) {
obj := NewDNSResolver(time.Second*10, []string{"8.8.8.8:53", "8.8.8.8:53"})
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr, dnsLoopbackAddr})
m := new(dns.Msg)
m.SetQuestion("letsencrypt.org.", dns.TypeSOA)
_, _, err := obj.ExchangeOne(m)
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA)
test.AssertNotError(t, err, "No message")
}
func TestDNSLookupsNoServer(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{})
_, _, err := obj.LookupTXT("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, _, _, err = obj.LookupHost("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, _, err = obj.LookupCNAME("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, _, err = obj.LookupCAA("letsencrypt.org")
test.AssertError(t, err, "No servers")
}
func TestDNSServFail(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
bad := "servfail.com"
_, _, err := obj.LookupTXT(bad)
test.AssertError(t, err, "LookupTXT didn't return an error")
_, _, err = obj.LookupCNAME(bad)
test.AssertError(t, err, "LookupCNAME didn't return an error")
_, _, _, err = obj.LookupHost(bad)
test.AssertError(t, err, "LookupHost didn't return an error")
// CAA lookup ignores validation failures from the resolver for now
// and returns an empty list of CAA records.
emptyCaa, _, err := obj.LookupCAA(bad)
test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records")
test.AssertNotError(t, err, "LookupCAA returned an error")
}
func TestDNSLookupTXT(t *testing.T) {
obj := NewDNSResolver(time.Second*10, []string{"8.8.8.8:53", "8.8.8.8:53"})
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
a, rtt, err := obj.LookupTXT("letsencrypt.org")
@ -52,44 +198,12 @@ func TestDNSLookupTXT(t *testing.T) {
test.AssertNotError(t, err, "No message")
}
func TestDNSLookupTXTNoServer(t *testing.T) {
obj := NewDNSResolver(time.Second*10, []string{})
_, _, err := obj.LookupTXT("letsencrypt.org")
test.AssertError(t, err, "No servers")
}
func TestDNSSEC(t *testing.T) {
goodServer := NewDNSResolver(time.Second*10, []string{"8.8.8.8:53"})
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn("sigfail.verteiltesysteme.net"), dns.TypeA)
_, _, err := goodServer.LookupDNSSEC(m)
test.AssertError(t, err, "DNSSEC failure")
_, ok := err.(DNSSECError)
test.Assert(t, ok, "Should have been a DNSSECError")
m.SetQuestion(dns.Fqdn("sigok.verteiltesysteme.net"), dns.TypeA)
_, _, err = goodServer.LookupDNSSEC(m)
test.AssertNotError(t, err, "DNSSEC should have worked")
badServer := NewDNSResolver(time.Second*10, []string{"127.0.0.1:99"})
_, _, err = badServer.LookupDNSSEC(m)
test.AssertError(t, err, "Should have failed")
_, ok = err.(DNSSECError)
test.Assert(t, !ok, "Shouldn't have been a DNSSECError")
}
func TestDNSLookupHost(t *testing.T) {
obj := NewDNSResolver(time.Second*10, []string{"8.8.8.8:53"})
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
ip, _, _, err := obj.LookupHost("sigfail.verteiltesysteme.net")
t.Logf("sigfail.verteiltesysteme.net - IP: %s, Err: %s", ip, err)
test.AssertError(t, err, "DNSSEC failure")
ip, _, _, err := obj.LookupHost("servfail.com")
t.Logf("servfail.com - IP: %s, Err: %s", ip, err)
test.AssertError(t, err, "Server failure")
test.Assert(t, len(ip) == 0, "Should not have IPs")
ip, _, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
@ -99,6 +213,46 @@ func TestDNSLookupHost(t *testing.T) {
ip, _, _, err = obj.LookupHost("cps.letsencrypt.org")
t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to be a CNAME")
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) > 0, "Should have IPs")
}
func TestDNSLookupCAA(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
caas, _, err := obj.LookupCAA("bracewel.net")
test.AssertNotError(t, err, "CAA lookup failed")
test.Assert(t, len(caas) > 0, "Should have CAA records")
caas, _, err = obj.LookupCAA("nonexistent.letsencrypt.org")
test.AssertNotError(t, err, "CAA lookup failed")
test.Assert(t, len(caas) == 0, "Shouldn't have CAA records")
caas, _, err = obj.LookupCAA("cname.example.com")
test.AssertNotError(t, err, "CAA lookup failed")
test.Assert(t, len(caas) > 0, "Should follow CNAME to find CAA")
}
func TestDNSLookupCNAME(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
target, _, err := obj.LookupCNAME("cps.letsencrypt.org")
test.AssertNotError(t, err, "CNAME lookup failed")
test.AssertEquals(t, target, "")
target, _, err = obj.LookupCNAME("cname.letsencrypt.org")
test.AssertNotError(t, err, "CNAME lookup failed")
test.AssertEquals(t, target, "cps.letsencrypt.org.")
}
func TestDNSLookupDNAME(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
target, _, err := obj.LookupDNAME("cps.letsencrypt.org")
test.AssertNotError(t, err, "DNAME lookup failed")
test.AssertEquals(t, target, "")
target, _, err = obj.LookupDNAME("dname.letsencrypt.org")
test.AssertNotError(t, err, "DNAME lookup failed")
test.AssertEquals(t, target, "cps.letsencrypt.org.")
}

View File

@ -7,10 +7,12 @@ package core
import (
"crypto/x509"
"net"
"net/http"
"time"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
)
@ -78,7 +80,7 @@ type RegistrationAuthority interface {
// ValidationAuthority defines the public interface for the Boulder VA
type ValidationAuthority interface {
// [RegistrationAuthority]
UpdateValidations(Authorization, int) error
UpdateValidations(Authorization, int, jose.JsonWebKey) error
CheckCAARecords(AcmeIdentifier) (bool, bool, error)
}
@ -101,6 +103,7 @@ type StorageGetter interface {
GetRegistration(int64) (Registration, error)
GetRegistrationByKey(jose.JsonWebKey) (Registration, error)
GetAuthorization(string) (Authorization, error)
GetLatestValidAuthorization(int64, AcmeIdentifier) (Authorization, error)
GetCertificate(string) (Certificate, error)
GetCertificateByShortSerial(string) (Certificate, error)
GetCertificateStatus(string) (CertificateStatus, error)
@ -135,3 +138,14 @@ type CertificateAuthorityDatabase interface {
IncrementAndGetSerial(*gorp.Transaction) (int64, error)
Begin() (*gorp.Transaction, error)
}
// DNSResolver defines methods used for DNS resolution
type DNSResolver interface {
ExchangeOne(string, uint16) (*dns.Msg, time.Duration, error)
LookupTXT(string) ([]string, time.Duration, error)
LookupHost(string) ([]net.IP, time.Duration, time.Duration, error)
LookupCNAME(string) (string, time.Duration, error)
LookupDNAME(string) (string, time.Duration, error)
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
LookupMX(string) ([]string, time.Duration, error)
}

View File

@ -8,20 +8,22 @@ package core
import (
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
"net"
"path/filepath"
"sort"
"strings"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
)
// AcmeStatus defines the state of a given authorization
type AcmeStatus string
// AcmeResource values identify different types of ACME resources
type AcmeResource string
// Buffer is a variable-length collection of bytes
type Buffer []byte
@ -56,6 +58,16 @@ const (
IdentifierDNS = IdentifierType("dns")
)
// The types of ACME resources
const (
ResourceNewReg = AcmeResource("new-reg")
ResourceNewAuthz = AcmeResource("new-authz")
ResourceNewCert = AcmeResource("new-cert")
ResourceRevokeCert = AcmeResource("revoke-cert")
ResourceRegistration = AcmeResource("reg")
ResourceChallenge = AcmeResource("challenge")
)
// These status are the states of OCSP
const (
OCSPStatusGood = OCSPStatus("good")
@ -65,7 +77,6 @@ const (
// Error types that can be used in ACME payloads
const (
ConnectionProblem = ProblemType("urn:acme:error:connection")
DNSSECProblem = ProblemType("urn:acme:error:dnssec")
MalformedProblem = ProblemType("urn:acme:error:malformed")
ServerInternalProblem = ProblemType("urn:acme:error:serverInternal")
TLSProblem = ProblemType("urn:acme:error:tls")
@ -75,14 +86,19 @@ const (
// These types are the available challenges
const (
ChallengeTypeSimpleHTTP = "simpleHttp"
ChallengeTypeDVSNI = "dvsni"
ChallengeTypeDNS = "dns"
ChallengeTypeRecoveryToken = "recoveryToken"
ChallengeTypeSimpleHTTP = "simpleHttp"
ChallengeTypeDVSNI = "dvsni"
ChallengeTypeDNS = "dns"
)
// The suffix appended to pseudo-domain names in DVSNI challenges
const DVSNISuffix = "acme.invalid"
// The label attached to DNS names in DNS challenges
const DNSPrefix = "_acme-challenge"
func (pd *ProblemDetails) Error() string {
return fmt.Sprintf("%v :: %v", pd.Type, pd.Detail)
return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail)
}
func cmpStrSlice(a, b []string) bool {
@ -141,22 +157,17 @@ type AcmeIdentifier struct {
Value string `json:"value"` // The identifier itself
}
// CertificateRequest is just a CSR together with
// URIs pointing to authorizations that should collectively
// authorize the certificate being requsted.
// CertificateRequest is just a CSR
//
// This type is never marshaled, since we only ever receive
// it from the client. So it carries some additional information
// that is useful internally. (We rely on Go's case-insensitive
// JSON unmarshal to properly unmarshal client requests.)
// This data is unmarshalled from JSON by way of rawCertificateRequest, which
// represents the actual structure received from the client.
type CertificateRequest struct {
CSR *x509.CertificateRequest // The CSR
Authorizations []AcmeURL // Links to Authorization over the account key
CSR *x509.CertificateRequest // The CSR
Bytes []byte // The original bytes of the CSR, for logging.
}
type rawCertificateRequest struct {
CSR JSONBuffer `json:"csr"` // The encoded CSR
Authorizations []AcmeURL `json:"authorizations"` // Authorizations
CSR JSONBuffer `json:"csr"` // The encoded CSR
}
// UnmarshalJSON provides an implementation for decoding CertificateRequest objects.
@ -172,15 +183,14 @@ func (cr *CertificateRequest) UnmarshalJSON(data []byte) error {
}
cr.CSR = csr
cr.Authorizations = raw.Authorizations
cr.Bytes = raw.CSR
return nil
}
// MarshalJSON provides an implementation for encoding CertificateRequest objects.
func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(rawCertificateRequest{
CSR: cr.CSR.Raw,
Authorizations: cr.Authorizations,
CSR: cr.CSR.Raw,
})
}
@ -193,16 +203,11 @@ type Registration struct {
// Account key to which the details are attached
Key jose.JsonWebKey `json:"key" db:"jwk"`
// Recovery Token is used to prove connection to an earlier transaction
RecoveryToken string `json:"recoveryToken" db:"recoveryToken"`
// Contact URIs
Contact []AcmeURL `json:"contact,omitempty" db:"contact"`
Contact []*AcmeURL `json:"contact,omitempty" db:"contact"`
// Agreement with terms of service
Agreement string `json:"agreement,omitempty" db:"agreement"`
LockCol int64 `json:"-"`
}
// MergeUpdate copies a subset of information from the input Registration
@ -237,19 +242,16 @@ type Challenge struct {
Validated *time.Time `json:"validated,omitempty"`
// A URI to which a response can be POSTed
URI AcmeURL `json:"uri"`
URI *AcmeURL `json:"uri"`
// Used by simpleHTTP, recoveryToken, and dns challenges
// Used by simpleHttp, dvsni, and dns challenges
Token string `json:"token,omitempty"`
// Used by simpleHTTP challenges
Path string `json:"path,omitempty"`
TLS *bool `json:"tls,omitempty"`
TLS *bool `json:"tls,omitempty"`
// Used by dvsni challenges
R string `json:"r,omitempty"`
S string `json:"s,omitempty"`
Nonce string `json:"nonce,omitempty"`
// Used by dns and dvsni challenges
Validation *jose.JsonWebSignature `json:"validation,omitempty"`
}
// IsSane checks the sanity of a challenge object before issued to the client
@ -262,29 +264,12 @@ func (ch Challenge) IsSane(completed bool) bool {
switch ch.Type {
case ChallengeTypeSimpleHTTP:
// check extra fields aren't used
if ch.R != "" || ch.S != "" || ch.Nonce != "" {
if ch.Validation != nil {
return false
}
// If the client has marked the challenge as completed, there should be a
// non-empty path provided. Otherwise there should be no default path.
if completed {
if ch.Path == "" {
return false
}
// Composed path should be a clean filepath (i.e. no double slashes, dot segments, etc)
vaURL := fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Path)
if vaURL != filepath.Clean(vaURL) {
return false
}
} else {
if ch.Path != "" {
return false
}
// TLS should set set to true by default
if ch.TLS == nil || !*ch.TLS {
return false
}
if completed && ch.TLS == nil {
return false
}
// check token is present, corrent length, and contains b64 encoded string
@ -295,41 +280,11 @@ func (ch Challenge) IsSane(completed bool) bool {
return false
}
case ChallengeTypeDVSNI:
// check extra fields aren't used
if ch.Path != "" || ch.Token != "" || ch.TLS != nil {
return false
}
if ch.Nonce == "" || len(ch.Nonce) != 32 {
return false
}
if _, err := hex.DecodeString(ch.Nonce); err != nil {
return false
}
// Check R & S are sane
if ch.R == "" || len(ch.R) != 43 {
return false
}
if _, err := B64dec(ch.R); err != nil {
return false
}
if completed {
if ch.S == "" || len(ch.S) != 43 {
return false
}
if _, err := B64dec(ch.S); err != nil {
return false
}
} else {
if ch.S != "" {
return false
}
}
// Same as DNS
fallthrough
case ChallengeTypeDNS:
// check extra fields aren't used
if ch.R != "" || ch.S != "" || ch.Nonce != "" || ch.TLS != nil {
if ch.TLS != nil {
return false
}
@ -341,6 +296,11 @@ func (ch Challenge) IsSane(completed bool) bool {
return false
}
// If completed, check that there's a validation object
if completed && ch.Validation == nil {
return false
}
default:
return false
}
@ -351,17 +311,24 @@ func (ch Challenge) IsSane(completed bool) bool {
// MergeResponse copies a subset of client-provided data to the current Challenge.
// Note: This method does not update the challenge on the left side of the '.'
func (ch Challenge) MergeResponse(resp Challenge) Challenge {
// Only override fields that are supposed to be client-provided
if len(ch.Path) == 0 {
ch.Path = resp.Path
}
switch ch.Type {
case ChallengeTypeSimpleHTTP:
// For simpleHttp, only "tls" is client-provided
// If "tls" is not provided, default to "true"
if resp.TLS != nil {
ch.TLS = resp.TLS
} else {
ch.TLS = new(bool)
*ch.TLS = true
}
if len(ch.S) == 0 {
ch.S = resp.S
}
if resp.TLS != nil {
ch.TLS = resp.TLS
case ChallengeTypeDVSNI:
fallthrough
case ChallengeTypeDNS:
// For dvsni and dns, only "validation" is client-provided
if resp.Validation != nil {
ch.Validation = resp.Validation
}
}
return ch
@ -444,11 +411,39 @@ type Certificate struct {
// * "revoked" - revoked
Status AcmeStatus `db:"status"`
Serial string `db:"serial"`
Digest string `db:"digest"`
DER JSONBuffer `db:"der"`
Issued time.Time `db:"issued"`
Expires time.Time `db:"expires"`
Serial string `db:"serial"`
Digest string `db:"digest"`
DER []byte `db:"der"`
Issued time.Time `db:"issued"`
Expires time.Time `db:"expires"`
}
type IssuedCertIdentifierData struct {
ReversedName string
Serial string
}
// IdentifierData holds information about what certificates are known for a
// given identifier. This is used to present Proof of Posession challenges in
// the case where a certificate already exists. The DB table holding
// IdentifierData rows contains information about certs issued by Boulder and
// also information about certs observed from third parties.
type IdentifierData struct {
ReversedName string `db:"reversedName"` // The label-wise reverse of an identifier, e.g. com.example or com.example.*
CertSHA1 string `db:"certSHA1"` // The hex encoding of the SHA-1 hash of a cert containing the identifier
}
// ExternalCert holds information about certificates issued by other CAs,
// obtained through Certificate Transparency, the SSL Observatory, or scans.io.
type ExternalCert struct {
SHA1 string `db:"sha1"` // The hex encoding of the SHA-1 hash of this cert
Issuer string `db:"issuer"` // The Issuer field of this cert
Subject string `db:"subject"` // The Subject field of this cert
NotAfter time.Time `db:"notAfter"` // Date after which this cert should be considered invalid
SPKI []byte `db:"spki"` // The hex encoding of the certificate's SubjectPublicKeyInfo in DER form
Valid bool `db:"valid"` // Whether this certificate was valid at LastUpdated time
EV bool `db:"ev"` // Whether this cert was EV valid
CertDER []byte `db:"rawDERCert"` // DER (binary) encoding of the raw certificate
}
// MatchesCSR tests the contents of a generated certificate to make sure
@ -555,6 +550,8 @@ type CertificateStatus struct {
// code for 'unspecified').
RevokedReason int `db:"revokedReason"`
LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
LockCol int64 `json:"-"`
}

View File

@ -7,9 +7,10 @@ package core
import (
"encoding/json"
"net/url"
"testing"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/test"
)
@ -21,16 +22,15 @@ func TestProblemDetails(t *testing.T) {
}
func TestRegistrationUpdate(t *testing.T) {
oldURL, _ := url.Parse("http://old.invalid")
newURL, _ := url.Parse("http://new.invalid")
oldURL, _ := ParseAcmeURL("http://old.invalid")
newURL, _ := ParseAcmeURL("http://new.invalid")
reg := Registration{
ID: 1,
Contact: []AcmeURL{AcmeURL(*oldURL)},
Contact: []*AcmeURL{oldURL},
Agreement: "",
}
update := Registration{
Contact: []AcmeURL{AcmeURL(*newURL)},
Contact: []*AcmeURL{newURL},
Agreement: "totally!",
}
@ -40,71 +40,32 @@ func TestRegistrationUpdate(t *testing.T) {
}
func TestSanityCheck(t *testing.T) {
tls := true
chall := Challenge{Type: ChallengeTypeSimpleHTTP, Status: StatusValid}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Status = StatusPending
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.R = "bad"
chall.S = "bad"
chall.Nonce = "bad"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall = Challenge{Type: ChallengeTypeSimpleHTTP, Path: "bad", Status: StatusPending}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = ""
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = "notlongenough"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+o!"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = "KQqLsiS5j0CONR_eUXTUSUDNVaHODtc-0pD6ACif7U4"
chall.Path = ""
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.TLS = &tls
test.Assert(t, chall.IsSane(false), "IsSane should be true")
types := []string{ChallengeTypeSimpleHTTP, ChallengeTypeDVSNI, ChallengeTypeDNS}
for _, challengeType := range types {
chall := Challenge{Type: challengeType, Status: StatusInvalid}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Status = StatusPending
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = ""
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = "notlongenough"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+o!"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Token = "KQqLsiS5j0CONR_eUXTUSUDNVaHODtc-0pD6ACif7U4"
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.Path = "../.."
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.Path = "/asd"
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.Path = "bad//test"
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.Path = "bad/./test"
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.Path = "good"
test.Assert(t, chall.IsSane(true), "IsSane should be true")
chall.Path = "good/test"
test.Assert(t, chall.IsSane(true), "IsSane should be true")
// Post-completion tests differ by type
if challengeType == ChallengeTypeSimpleHTTP {
tls := true
chall.TLS = &tls
test.Assert(t, chall.IsSane(false), "IsSane should be true")
} else if challengeType == ChallengeTypeDVSNI || challengeType == ChallengeTypeDNS {
chall.Validation = new(jose.JsonWebSignature)
test.Assert(t, chall.IsSane(true), "IsSane should be true")
}
}
chall = Challenge{Type: ChallengeTypeDVSNI, Status: StatusPending}
chall.Path = "bad"
chall.Token = "bad"
chall.TLS = &tls
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall = Challenge{Type: ChallengeTypeDVSNI, Status: StatusPending}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Nonce = "wutwut"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Nonce = "!2345678901234567890123456789012"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.Nonce = "12345678901234567890123456789012"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.R = "notlongenough"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.R = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+o!"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
chall.R = "KQqLsiS5j0CONR_eUXTUSUDNVaHODtc-0pD6ACif7U4"
test.Assert(t, chall.IsSane(false), "IsSane should be true")
chall.S = "anything"
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.S = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+o!"
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
chall.S = "KQqLsiS5j0CONR_eUXTUSUDNVaHODtc-0pD6ACif7U4"
test.Assert(t, chall.IsSane(true), "IsSane should be true")
chall = Challenge{Type: "bogus", Status: StatusPending}
chall := Challenge{Type: "bogus", Status: StatusPending}
test.Assert(t, !chall.IsSane(false), "IsSane should be false")
test.Assert(t, !chall.IsSane(true), "IsSane should be false")
}

View File

@ -19,13 +19,14 @@ import (
"encoding/json"
"errors"
"fmt"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
blog "github.com/letsencrypt/boulder/log"
"hash"
"io"
"math/big"
"net/url"
"strings"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
blog "github.com/letsencrypt/boulder/log"
)
// Package Variables Variables
@ -173,13 +174,22 @@ func KeyDigestEquals(j, k crypto.PublicKey) bool {
// AcmeURL is a URL that automatically marshal/unmarshal to JSON strings
type AcmeURL url.URL
func (u AcmeURL) String() string {
url := url.URL(u)
return url.String()
func ParseAcmeURL(s string) (*AcmeURL, error) {
u, err := url.Parse(s)
if err != nil {
return nil, err
}
au := AcmeURL(*u)
return &au, nil
}
func (u *AcmeURL) String() string {
uu := url.URL(*u)
return uu.String()
}
// PathSegments splits an AcmeURL into segments on the '/' characters
func (u AcmeURL) PathSegments() (segments []string) {
func (u *AcmeURL) PathSegments() (segments []string) {
segments = strings.Split(u.Path, "/")
if len(segments) > 0 && len(segments[0]) == 0 {
segments = segments[1:]
@ -188,8 +198,8 @@ func (u AcmeURL) PathSegments() (segments []string) {
}
// MarshalJSON encodes an AcmeURL for transfer
func (u AcmeURL) MarshalJSON() ([]byte, error) {
uu := url.URL(u)
func (u *AcmeURL) MarshalJSON() ([]byte, error) {
uu := url.URL(*u)
return json.Marshal(uu.String())
}

View File

@ -8,7 +8,7 @@ package core
import (
"encoding/json"
"fmt"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/test"
"math"
"math/big"
@ -54,13 +54,13 @@ func TestBuildID(t *testing.T) {
const JWK1JSON = `{
"kty": "RSA",
"n": "vuc785P8lBj3fUxyZchF_uZw6WtbxcorqgTyq-qapF5lrO1U82Tp93rpXlmctj6fyFHBVVB5aXnUHJ7LZeVPod7Wnfl8p5OyhlHQHC8BnzdzCqCMKmWZNX5DtETDId0qzU7dPzh0LP0idt5buU7L9QNaabChw3nnaL47iu_1Di5Wp264p2TwACeedv2hfRDjDlJmaQXuS8Rtv9GnRWyC9JBu7XmGvGDziumnJH7Hyzh3VNu-kSPQD3vuAFgMZS6uUzOztCkT0fpOalZI6hqxtWLvXUMj-crXrn-Maavz8qRhpAyp5kcYk3jiHGgQIi7QSK2JIdRJ8APyX9HlmTN5AQ",
"e": "AAEAAQ"
"e": "AQAB"
}`
const JWK1Digest = `ul04Iq07ulKnnrebv2hv3yxCGgVvoHs8hjq2tVKx3mc=`
const JWK2JSON = `{
"kty":"RSA",
"n":"yTsLkI8n4lg9UuSKNRC0UPHsVjNdCYk8rGXIqeb_rRYaEev3D9-kxXY8HrYfGkVt5CiIVJ-n2t50BKT8oBEMuilmypSQqJw0pCgtUm-e6Z0Eg3Ly6DMXFlycyikegiZ0b-rVX7i5OCEZRDkENAYwFNX4G7NNCwEZcH7HUMUmty9dchAqDS9YWzPh_dde1A9oy9JMH07nRGDcOzIh1rCPwc71nwfPPYeeS4tTvkjanjeigOYBFkBLQuv7iBB4LPozsGF1XdoKiIIi-8ye44McdhOTPDcQp3xKxj89aO02pQhBECv61rmbPinvjMG9DYxJmZvjsKF4bN2oy0DxdC1jDw",
"e":"AAEAAQ"
"e":"AQAB"
}`
func TestKeyDigest(t *testing.T) {

View File

@ -2,4 +2,4 @@ These `.sql` files define the table layout, indicies, relationships, and users d
## Notes
Currently, if you use MySQL / MariaDB with Boulder, you must manually append `?parseTime=true"` onto the end of the `dbName` configuration fields for each entry. This is related to [Issue #242](https://github.com/letsencrypt/boulder/issues/242).
Currently, if you use MySQL / MariaDB with Boulder, you must manually append `?parseTime=true"` onto the end of the `dbConnect` configuration fields for each entry. This is related to [Issue #242](https://github.com/letsencrypt/boulder/issues/242).

View File

@ -94,3 +94,20 @@ CREATE TABLE `pending_authz` (
CONSTRAINT `regId_pending_authz` FOREIGN KEY (`registrationID`) REFERENCES `registrations` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `identifierData` (
`reversedName` varchar(255) NOT NULL,
`certSHA1` varchar(40) NOT NULL,
UNIQUE INDEX (certSha1, reversedName)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `externalCerts` (
`sha1` varchar(40) NOT NULL,
`issuer` text DEFAULT NULL,
`subject` text DEFAULT NULL,
`notAfter` datetime DEFAULT NULL,
`spki` blob DEFAULT NULL,
`valid` tinyint(1) DEFAULT NULL,
`ev` tinyint(1) DEFAULT NULL,
`rawDERCert` blob DEFAULT NULL,
UNIQUE INDEX (sha1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

136
docs/rabbitmq_acl_configure.sh Executable file
View File

@ -0,0 +1,136 @@
#!/bin/bash
# Copyright 2015 ISRG. All rights reserved
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This file creates individual AMQP accounts for each Boulder component,
# and sets restrictive access controls on those accounts.
#
# You can use this tool without any configuration to produce users named
# [am, ca, sa, ra, va, wfe, ocsp-updater] which all have the password "guest".
# You can also customize this tool by creating a config file that will be
# sourced. By default this file is obtained from $HOME/.rabbitmq_config, but
# you can override the config file path using the environment variable
# RABBITMQ_ACL_CONFIG, such as:
#
# $ RABBITMQ_ACL_CONFIG=myconfig ./rabbitmq_acl_configure.sh
# VARIABLES
PORT=15672
HOST=localhost
VHOST="/"
EXTRA=""
RABBIT_ADMIN=$(which rabbitmqadmin)
# USER NAMES
USER_BOULDER_AM="am"
USER_BOULDER_CA="ca"
USER_BOULDER_SA="sa"
USER_BOULDER_RA="ra"
USER_BOULDER_VA="va"
USER_BOULDER_WFE="wfe"
USER_BOULDER_OCSP="ocsp-updater"
# PASSWORDS
PASS_BOULDER_AM="guest"
PASS_BOULDER_CA="guest"
PASS_BOULDER_SA="guest"
PASS_BOULDER_RA="guest"
PASS_BOULDER_VA="guest"
PASS_BOULDER_WFE="guest"
PASS_BOULDER_OCSP="guest"
# To use different options, you should create an override
# file with whatever changes you want for the above variables
RABBITMQ_ACL_CONFIG=${RABBITMQ_ACL_CONFIG:-$HOME/.rabbitmq_config}
if [ -r "${RABBITMQ_ACL_CONFIG}" ] ; then
echo "Loading overrides from ${RABBITMQ_ACL_CONFIG}..."
source "${RABBITMQ_ACL_CONFIG}"
fi
if ! [ -x "${RABBIT_ADMIN}" ] ; then
echo "Could not locate rabbitmqadmin; please set RABBIT_ADMIN in your ${RABBITMQ_ACL_CONFIG} file."
exit 1
fi
run() {
echo $*
$*
}
admin() {
run ${RABBIT_ADMIN} -H ${HOST} -P ${PORT} -V ${VHOST} ${EXTRA} $*
}
admin declare queue name="Monitor" durable=false
admin declare queue name="CA.server" durable=false
admin declare queue name="SA.server" durable=false
admin declare queue name="RA.server" durable=false
admin declare queue name="VA.server" durable=false
admin declare exchange name="boulder" type=topic durable=false
# Bind the wildcard topic (#) to Monitor, asking the server to copy all messages
# and place them in the Montior queue.
admin declare binding source="boulder" destination="Monitor" routing_key="#"
admin declare user name=${USER_BOULDER_AM} password=${PASS_BOULDER_AM} tags=""
admin declare user name=${USER_BOULDER_CA} password=${PASS_BOULDER_CA} tags=""
admin declare user name=${USER_BOULDER_SA} password=${PASS_BOULDER_SA} tags=""
admin declare user name=${USER_BOULDER_RA} password=${PASS_BOULDER_RA} tags=""
admin declare user name=${USER_BOULDER_VA} password=${PASS_BOULDER_VA} tags=""
admin declare user name=${USER_BOULDER_WFE} password=${PASS_BOULDER_WFE} tags=""
admin declare user name=${USER_BOULDER_OCSP} password=${PASS_BOULDER_OCSP} tags=""
##################################################
## Permissions RegExes ##
##################################################
## Mystified? These are applied by the server ##
## to various operations on queue names per ##
## the decoder matrix here: ##
## https://www.rabbitmq.com/access-control.html ##
##################################################
# AM is read-only, and uses a predeclared Queue.
admin declare permission vhost=${VHOST} user=${USER_BOULDER_AM} \
configure="^$" \
write="^$" \
read="^Monitor$"
# VA uses VA.server, as well as dynamic queues named VA->RA.{hostname}.
admin declare permission vhost=${VHOST} user=${USER_BOULDER_VA} \
configure="^(VA\.server|VA->RA.*)$" \
write="^(boulder|VA\.server|VA->RA.*)$" \
read="^(boulder|VA\.server|VA->RA.*)$"
# RA uses RA.server, and RA->CA, RA->SA, RA->VA
admin declare permission vhost=${VHOST} user=${USER_BOULDER_RA} \
configure="^(RA\.server|RA->(CA|SA|VA).*)$" \
write="^(boulder|RA\.server|RA->(CA|SA|VA).*)$" \
read="^(boulder|RA\.server|RA->(CA|SA|VA).*)$"
# CA uses CA.server, and CA->SA
admin declare permission vhost=${VHOST} user=${USER_BOULDER_CA} \
configure="^(CA\.server|CA->SA.*)$" \
write="^(boulder|CA\.server|CA->SA.*)$" \
read="^(boulder|CA\.server|CA->SA.*)$"
# SA uses only SA.server
admin declare permission vhost=${VHOST} user=${USER_BOULDER_SA} \
configure="^SA\.server$" \
write="^(boulder|SA\.server)$" \
read="^(boulder|SA\.server)$"
# WFE uses WFE->RA and WFE->SA
admin declare permission vhost=${VHOST} user=${USER_BOULDER_WFE} \
configure="^(WFE->(RA|SA).*)$" \
write="^(boulder|WFE->(RA|SA).*)$" \
read="^(boulder|WFE->(RA|SA).*)$"
# OCSP uses only OCSP->CA
admin declare permission vhost=${VHOST} user=${USER_BOULDER_OCSP} \
configure="^(OCSP->CA.*)$" \
write="^(boulder|OCSP->CA.*)$" \
read="^(boulder|OCSP->CA.*)$"

Some files were not shown because too many files have changed in this diff Show More