Merge master
This commit is contained in:
commit
8789f925cc
|
|
@ -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]
|
||||
|
|
|
|||
35
.travis.yml
35
.travis.yml
|
|
@ -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
|
||||
|
|
|
|||
18
DESIGN.md
18
DESIGN.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
19
Godeps/_workspace/src/github.com/cactus/go-statsd-client/LICENSE.md
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/cactus/go-statsd-client/LICENSE.md
generated
vendored
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
7
Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs11key/pkcs11key.go
generated
vendored
7
Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs11key/pkcs11key.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// +build !pkcs11
|
||||
// +build nopkcs11
|
||||
|
||||
package pkcs11
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// +build pkcs11
|
||||
// +build !nopkcs11
|
||||
|
||||
package pkcs11
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// +build !pkcs11
|
||||
// +build nopkcs11
|
||||
|
||||
package pkcs11
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
189
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/cbc_hmac.go
generated
vendored
Normal file
189
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/cbc_hmac.go
generated
vendored
Normal 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
|
||||
}
|
||||
458
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/cbc_hmac_test.go
generated
vendored
Normal file
458
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/cbc_hmac_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
75
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/concat_kdf.go
generated
vendored
Normal file
75
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/concat_kdf.go
generated
vendored
Normal 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
|
||||
}
|
||||
148
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/concat_kdf_test.go
generated
vendored
Normal file
148
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/concat_kdf_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
51
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/ecdh_es.go
generated
vendored
Normal file
51
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/ecdh_es.go
generated
vendored
Normal 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
|
||||
}
|
||||
98
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/ecdh_es_test.go
generated
vendored
Normal file
98
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/ecdh_es_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
109
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/key_wrap.go
generated
vendored
Normal file
109
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/key_wrap.go
generated
vendored
Normal 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
|
||||
}
|
||||
133
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/key_wrap_test.go
generated
vendored
Normal file
133
Godeps/_workspace/src/github.com/letsencrypt/go-jose/cipher/key_wrap_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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]")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
88
Godeps/_workspace/src/github.com/letsencrypt/go-jose/jose-util/jose-util.t
generated
vendored
Normal file
88
Godeps/_workspace/src/github.com/letsencrypt/go-jose/jose-util/jose-util.t
generated
vendored
Normal 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
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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("###")
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +301,7 @@ func (ctx symmetricMac) signPayload(payload []byte, alg SignatureAlgorithm) (Sig
|
|||
}
|
||||
|
||||
return Signature{
|
||||
signature: mac,
|
||||
Signature: mac,
|
||||
protected: &rawHeader{},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
46
Makefile
46
Makefile
|
|
@ -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))
|
||||
|
|
|
|||
27
README.md
27
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
})))
|
||||
}
|
||||
|
|
@ -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,
|
||||
})))
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
203
cmd/shell.go
203
cmd/shell.go
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
218
core/dns.go
218
core/dns.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
252
core/dns_test.go
252
core/dns_test.go
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
207
core/objects.go
207
core/objects.go
|
|
@ -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:"-"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
26
core/util.go
26
core/util.go
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue