Merge branch 'goodkey' of github.com:letsencrypt/boulder into goodkey
Conflicts: ca/certificate-authority.go core/good_key.go core/good_key_test.go
This commit is contained in:
commit
cb00816e48
|
@ -182,9 +182,16 @@ func (ca *CertificateAuthorityImpl) RevokeCertificate(serial string) (err error)
|
||||||
|
|
||||||
// IssueCertificate attempts to convert a CSR into a signed Certificate, while
|
// IssueCertificate attempts to convert a CSR into a signed Certificate, while
|
||||||
// enforcing all policies.
|
// enforcing all policies.
|
||||||
func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest) (core.Certificate, error) {
|
func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest) (cert core.Certificate, err error) {
|
||||||
emptyCert := core.Certificate{}
|
emptyCert := core.Certificate{}
|
||||||
var err error
|
key, ok := csr.PublicKey.(crypto.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return emptyCert, fmt.Errorf("Invalid public key in CSR.")
|
||||||
|
}
|
||||||
|
if !core.GoodKey(key) {
|
||||||
|
return emptyCert, fmt.Errorf("Invalid public key in CSR.")
|
||||||
|
}
|
||||||
|
|
||||||
// XXX Take in authorizations and verify that union covers CSR?
|
// XXX Take in authorizations and verify that union covers CSR?
|
||||||
// Pull hostnames from CSR
|
// Pull hostnames from CSR
|
||||||
hostNames := csr.DNSNames // DNSNames + CN from CSR
|
hostNames := csr.DNSNames // DNSNames + CN from CSR
|
||||||
|
@ -194,7 +201,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
||||||
} else if len(hostNames) > 0 {
|
} else if len(hostNames) > 0 {
|
||||||
commonName = hostNames[0]
|
commonName = hostNames[0]
|
||||||
} else {
|
} else {
|
||||||
err = errors.New("Cannot issue a certificate without a hostname.")
|
err = fmt.Errorf("Cannot issue a certificate without a hostname.")
|
||||||
ca.log.WarningErr(err)
|
ca.log.WarningErr(err)
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
}
|
}
|
||||||
|
@ -205,14 +212,14 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
||||||
|
|
||||||
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: commonName}
|
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: commonName}
|
||||||
if err = ca.PA.WillingToIssue(identifier); err != nil {
|
if err = ca.PA.WillingToIssue(identifier); err != nil {
|
||||||
err = errors.New("Policy forbids issuing for name " + commonName)
|
err = fmt.Errorf("Policy forbids issuing for name %s", commonName)
|
||||||
ca.log.AuditErr(err)
|
ca.log.AuditErr(err)
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
}
|
}
|
||||||
for _, name := range hostNames {
|
for _, name := range hostNames {
|
||||||
identifier = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: name}
|
identifier = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: name}
|
||||||
if err = ca.PA.WillingToIssue(identifier); err != nil {
|
if err = ca.PA.WillingToIssue(identifier); err != nil {
|
||||||
err = errors.New("Policy forbids issuing for name " + name)
|
err = fmt.Errorf("Policy forbids issuing for name %s", name)
|
||||||
ca.log.AuditErr(err)
|
ca.log.AuditErr(err)
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
}
|
}
|
||||||
|
@ -265,7 +272,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
||||||
}
|
}
|
||||||
certDER := block.Bytes
|
certDER := block.Bytes
|
||||||
|
|
||||||
cert := core.Certificate{
|
cert = core.Certificate{
|
||||||
DER: certDER,
|
DER: certDER,
|
||||||
Status: core.StatusValid,
|
Status: core.StatusValid,
|
||||||
}
|
}
|
||||||
|
@ -281,5 +288,5 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
ca.DB.Commit()
|
ca.DB.Commit()
|
||||||
return cert, nil
|
return cert, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -427,6 +427,14 @@ func TestIssueCertificate(t *testing.T) {
|
||||||
test.Assert(t, certStatus.Status == core.OCSPStatusGood, "Certificate status was not good")
|
test.Assert(t, certStatus.Status == core.OCSPStatusGood, "Certificate status was not good")
|
||||||
test.Assert(t, certStatus.SubscriberApproved == false, "Subscriber shouldn't have approved cert yet.")
|
test.Assert(t, certStatus.SubscriberApproved == false, "Subscriber shouldn't have approved cert yet.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestRejectNoName(t *testing.T) {
|
||||||
|
cadb, storageAuthority, caConfig := setup(t)
|
||||||
|
ca, err := NewCertificateAuthorityImpl(cadb, caConfig)
|
||||||
|
test.AssertNotError(t, err, "Failed to create CA")
|
||||||
|
ca.SA = storageAuthority
|
||||||
|
|
||||||
// Test that the CA rejects CSRs with no names
|
// Test that the CA rejects CSRs with no names
|
||||||
csrDER, _ := hex.DecodeString(NO_NAME_CSR_HEX)
|
csrDER, _ := hex.DecodeString(NO_NAME_CSR_HEX)
|
||||||
|
@ -436,3 +444,19 @@ func TestIssueCertificate(t *testing.T) {
|
||||||
t.Errorf("CA improperly agreed to create a certificate with no name")
|
t.Errorf("CA improperly agreed to create a certificate with no name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShortKey(t *testing.T) {
|
||||||
|
cadb, storageAuthority, caConfig := setup(t)
|
||||||
|
ca, err := NewCertificateAuthorityImpl(cadb, caConfig)
|
||||||
|
ca.SA = storageAuthority
|
||||||
|
|
||||||
|
csrDER, err := ioutil.ReadFile("shortkey-csr.der")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to read shortkey-csr.der")
|
||||||
|
}
|
||||||
|
csr, _ := x509.ParseCertificateRequest(csrDER)
|
||||||
|
_, err = ca.IssueCertificate(*csr)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("CA improperly created a certificate with short key.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
|
@ -11,12 +11,19 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"math/big"
|
||||||
blog "github.com/letsencrypt/boulder/log"
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// To generate, run: primes 2 752 | tr '\n' ,
|
||||||
|
var smallPrimes = []int64{
|
||||||
|
2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,
|
||||||
|
}
|
||||||
|
|
||||||
// GoodKey returns true iff the key is acceptable for both TLS use and account
|
// GoodKey returns true iff the key is acceptable for both TLS use and account
|
||||||
// key use (our requirements are the same for either one), according to basic
|
// key use (our requirements are the same for either one), according to basic
|
||||||
// strength and algorithm checking.
|
// strength and algorithm checking.
|
||||||
|
// TODO: Support JsonWebKeys once go-jose migration is done.
|
||||||
func GoodKey(key crypto.PublicKey) bool {
|
func GoodKey(key crypto.PublicKey) bool {
|
||||||
log := blog.GetAuditLogger()
|
log := blog.GetAuditLogger()
|
||||||
switch t := key.(type) {
|
switch t := key.(type) {
|
||||||
|
@ -64,8 +71,16 @@ func GoodKeyRSA(key rsa.PublicKey) bool {
|
||||||
log.Debug(fmt.Sprintf("Key exponent is too small: %d", key.E))
|
log.Debug(fmt.Sprintf("Key exponent is too small: %d", key.E))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// TODO: The modulus SHOULD also have the following
|
// The modulus SHOULD also have the following characteristics: an odd
|
||||||
// characteristics: an odd number, not the power of a prime,
|
// number, not the power of a prime, and have no factors smaller than 752.
|
||||||
// and have no factors smaller than 752.
|
// TODO: We don't yet check for "power of a prime."
|
||||||
|
for _, prime := range smallPrimes {
|
||||||
|
var result big.Int
|
||||||
|
result.Mod(modulus, big.NewInt(prime))
|
||||||
|
if result.Sign() == 0 {
|
||||||
|
log.Debug(fmt.Sprintf("Key divisible by small prime: %d", prime))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,370 @@
|
||||||
|
// 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 core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
|
||||||
|
"time"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IdentifierType string
|
||||||
|
type AcmeStatus string
|
||||||
|
type OCSPStatus string
|
||||||
|
type Buffer []byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusUnknown = AcmeStatus("unknown") // Unknown status; the default
|
||||||
|
StatusPending = AcmeStatus("pending") // In process; client has next action
|
||||||
|
StatusProcessing = AcmeStatus("processing") // In process; server has next action
|
||||||
|
StatusValid = AcmeStatus("valid") // Validation succeeded
|
||||||
|
StatusInvalid = AcmeStatus("invalid") // Validation failed
|
||||||
|
StatusRevoked = AcmeStatus("revoked") // Object no longer valid
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OCSPStatusGood = OCSPStatus("good")
|
||||||
|
OCSPStatusRevoked = OCSPStatus("revoked")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChallengeTypeSimpleHTTPS = "simpleHttps"
|
||||||
|
ChallengeTypeDVSNI = "dvsni"
|
||||||
|
ChallengeTypeDNS = "dns"
|
||||||
|
ChallengeTypeRecoveryToken = "recoveryToken"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IdentifierDNS = IdentifierType("dns")
|
||||||
|
)
|
||||||
|
|
||||||
|
// An AcmeIdentifier encodes an identifier that can
|
||||||
|
// be validated by ACME. The protocol allows for different
|
||||||
|
// types of identifier to be supported (DNS names, IP
|
||||||
|
// addresses, etc.), but currently we only support
|
||||||
|
// domain names.
|
||||||
|
type AcmeIdentifier struct {
|
||||||
|
Type IdentifierType `json:"type"` // The type of identifier being encoded
|
||||||
|
Value string `json:"value"` // The identifier itself
|
||||||
|
}
|
||||||
|
|
||||||
|
// An ACME certificate request is just a CSR together with
|
||||||
|
// URIs pointing to authorizations that should collectively
|
||||||
|
// authorize the certificate being requsted.
|
||||||
|
//
|
||||||
|
// 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.)
|
||||||
|
type CertificateRequest struct {
|
||||||
|
CSR *x509.CertificateRequest // The CSR
|
||||||
|
Authorizations []AcmeURL // Links to Authorization over the account key
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawCertificateRequest struct {
|
||||||
|
CSR []byte `json:"csr"` // The encoded CSR
|
||||||
|
Authorizations []AcmeURL `json:"authorizations"` // Authorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *CertificateRequest) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw rawCertificateRequest
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err := x509.ParseCertificateRequest(raw.CSR)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.CSR = csr
|
||||||
|
cr.Authorizations = raw.Authorizations
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(rawCertificateRequest{
|
||||||
|
CSR: cr.CSR.Raw,
|
||||||
|
Authorizations: cr.Authorizations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration objects represent non-public metadata attached
|
||||||
|
// to account keys.
|
||||||
|
type Registration struct {
|
||||||
|
// Unique identifier
|
||||||
|
ID string `json:"-" db:"id"`
|
||||||
|
|
||||||
|
// Account key to which the details are attached
|
||||||
|
Key jose.JsonWebKey `json:"key" db:"key"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
|
// Agreement with terms of service
|
||||||
|
Agreement string `json:"agreement,omitempty" db:"agreement"`
|
||||||
|
|
||||||
|
//
|
||||||
|
Thumbprint string `db:"thumbprint"`
|
||||||
|
|
||||||
|
LockCol int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registration) MergeUpdate(input Registration) {
|
||||||
|
if len(input.Contact) > 0 {
|
||||||
|
r.Contact = input.Contact
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Test to make sure this has the proper value
|
||||||
|
if len(input.Agreement) > 0 {
|
||||||
|
r.Agreement = input.Agreement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rather than define individual types for different types of
|
||||||
|
// challenge, we just throw all the elements into one bucket,
|
||||||
|
// together with the common metadata elements.
|
||||||
|
type Challenge struct {
|
||||||
|
// The type of challenge
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
// The status of this challenge
|
||||||
|
Status AcmeStatus `json:"status,omitempty"`
|
||||||
|
|
||||||
|
// If successful, the time at which this challenge
|
||||||
|
// was completed by the server.
|
||||||
|
Validated *time.Time `json:"validated,omitempty"`
|
||||||
|
|
||||||
|
// A URI to which a response can be POSTed
|
||||||
|
URI AcmeURL `json:"uri"`
|
||||||
|
|
||||||
|
// Used by simpleHTTPS, recoveryToken, and dns challenges
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
|
||||||
|
// Used by simpleHTTPS challenges
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
|
||||||
|
// Used by dvsni challenges
|
||||||
|
R string `json:"r,omitempty"`
|
||||||
|
S string `json:"s,omitempty"`
|
||||||
|
Nonce string `json:"nonce,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the sanity of a challenge object before issued to the client (completed = false)
|
||||||
|
// and before validation (completed = true).
|
||||||
|
func (ch Challenge) IsSane(completed bool) bool {
|
||||||
|
if ch.Status != StatusPending {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ch.Type {
|
||||||
|
case ChallengeTypeSimpleHTTPS:
|
||||||
|
// check extra fields aren't used
|
||||||
|
if ch.R != "" || ch.S != "" || ch.Nonce != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if completed {
|
||||||
|
// see if ch.Path starts with /.well-known/acme-challenge/
|
||||||
|
if ch.Path == "" || !strings.HasPrefix(ch.Path, "/.well-known/acme-challenge/") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ch.Path != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check token is present, corrent length, and contains b64 encoded string
|
||||||
|
if ch.Token == "" || len(ch.Token) != 43 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := B64dec(ch.Token); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case ChallengeTypeDVSNI:
|
||||||
|
// check extra fields aren't used
|
||||||
|
if ch.Path != "" || ch.Token != "" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge a client-provide response to a challenge with the issued challenge
|
||||||
|
// TODO: Remove return type from this method
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ch.S) == 0 {
|
||||||
|
ch.S = resp.S
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// An ACME authorization object represents the authorization
|
||||||
|
// of an account key holder to act on behalf of a domain. This
|
||||||
|
// struct is intended to be used both internally and for JSON
|
||||||
|
// marshaling on the wire. Any fields that should be suppressed
|
||||||
|
// on the wire (e.g., ID) must be made empty before marshaling.
|
||||||
|
type Authorization struct {
|
||||||
|
// An identifier for this authorization, unique across
|
||||||
|
// authorizations and certificates within this instance.
|
||||||
|
ID string `json:"id,omitempty" db:"id"`
|
||||||
|
|
||||||
|
// The identifier for which authorization is being given
|
||||||
|
Identifier AcmeIdentifier `json:"identifier,omitempty" db:"identifier"`
|
||||||
|
|
||||||
|
// The account key that is authorized for the identifier
|
||||||
|
Key jose.JsonWebKey `json:"key,omitempty" db:"key"`
|
||||||
|
|
||||||
|
// The status of the validation of this authorization
|
||||||
|
Status AcmeStatus `json:"status,omitempty" db:"status"`
|
||||||
|
|
||||||
|
// The date after which this authorization will be no
|
||||||
|
// longer be considered valid
|
||||||
|
Expires time.Time `json:"expires,omitempty" db:"expires"`
|
||||||
|
|
||||||
|
// An array of challenges objects used to validate the
|
||||||
|
// applicant's control of the identifier. For authorizations
|
||||||
|
// in process, these are challenges to be fulfilled; for
|
||||||
|
// final authorizations, they describe the evidence that
|
||||||
|
// the server used in support of granting the authorization.
|
||||||
|
Challenges []Challenge `json:"challenges,omitempty" db:"challenges"`
|
||||||
|
|
||||||
|
// The server may suggest combinations of challenges if it
|
||||||
|
// requires more than one challenge to be completed.
|
||||||
|
Combinations [][]int `json:"combinations,omitempty" db:"combinations"`
|
||||||
|
|
||||||
|
// The client may provide contact URIs to allow the server
|
||||||
|
// to push information to it.
|
||||||
|
Contact []AcmeURL `json:"contact,omitempty" db:"contact"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate objects are entirely internal to the server. The only
|
||||||
|
// thing exposed on the wire is the certificate itself.
|
||||||
|
type Certificate struct {
|
||||||
|
// The encoded, signed certificate
|
||||||
|
<<<<<<< HEAD
|
||||||
|
DER []byte
|
||||||
|
=======
|
||||||
|
DER jose.JsonBuffer `db:"-"`
|
||||||
|
>>>>>>> master
|
||||||
|
|
||||||
|
// The parsed version of DER. Useful for extracting things like serial number.
|
||||||
|
ParsedCertificate *x509.Certificate `db:"-"`
|
||||||
|
|
||||||
|
// The revocation status of the certificate.
|
||||||
|
// * "valid" - not revoked
|
||||||
|
// * "revoked" - revoked
|
||||||
|
Status AcmeStatus `db:"status"`
|
||||||
|
|
||||||
|
Serial string `db:"serial"`
|
||||||
|
Digest string `db:"digest"`
|
||||||
|
Content []byte `db:"content"`
|
||||||
|
Issued time.Time `db:"issued"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertificateStatus structs are internal to the server. They represent the
|
||||||
|
// latest data about the status of the certificate, required for OCSP updating
|
||||||
|
// and for validating that the subscriber has accepted the certificate.
|
||||||
|
type CertificateStatus struct {
|
||||||
|
Serial string `db:"serial"`
|
||||||
|
|
||||||
|
// subscriberApproved: true iff the subscriber has posted back to the server
|
||||||
|
// that they accept the certificate, otherwise 0.
|
||||||
|
SubscriberApproved bool `db:"subscriberApproved"`
|
||||||
|
|
||||||
|
// status: 'good' or 'revoked'. Note that good, expired certificates remain
|
||||||
|
// with status 'good' but don't necessarily get fresh OCSP responses.
|
||||||
|
Status OCSPStatus `db:"status"`
|
||||||
|
|
||||||
|
// ocspLastUpdated: The date and time of the last time we generated an OCSP
|
||||||
|
// response. If we have never generated one, this has the zero value of
|
||||||
|
// time.Time, i.e. Jan 1 1970.
|
||||||
|
OCSPLastUpdated time.Time `db:"ocspLastUpdated"`
|
||||||
|
|
||||||
|
// revokedDate: If status is 'revoked', this is the date and time it was
|
||||||
|
// revoked. Otherwise it has the zero value of time.Time, i.e. Jan 1 1970.
|
||||||
|
RevokedDate time.Time `db:"revokedDate"`
|
||||||
|
|
||||||
|
// revokedReason: If status is 'revoked', this is the reason code for the
|
||||||
|
// revocation. Otherwise it is zero (which happens to be the reason
|
||||||
|
// code for 'unspecified').
|
||||||
|
RevokedReason int `db:"revokedReason"`
|
||||||
|
|
||||||
|
LockCol int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// A large table of OCSP responses. This contains all historical OCSP
|
||||||
|
// responses we've signed, is append-only, and is likely to get quite
|
||||||
|
// large. We'll probably want administratively truncate it at some point.
|
||||||
|
type OcspResponse struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
|
||||||
|
// serial: Same as certificate serial.
|
||||||
|
Serial string `db:"serial"`
|
||||||
|
|
||||||
|
// createdAt: The date the response was signed.
|
||||||
|
CreatedAt time.Time `db:"createdAt"`
|
||||||
|
|
||||||
|
// response: The encoded and signed CRL.
|
||||||
|
Response []byte `db:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// A large table of signed CRLs. This contains all historical CRLs
|
||||||
|
// we've signed, is append-only, and is likely to get quite large.
|
||||||
|
type Crl struct {
|
||||||
|
// serial: Same as certificate serial.
|
||||||
|
Serial string `db:"serial"`
|
||||||
|
|
||||||
|
// createdAt: The date the CRL was signed.
|
||||||
|
CreatedAt time.Time `db:"createdAt"`
|
||||||
|
|
||||||
|
// crl: The encoded and signed CRL.
|
||||||
|
Crl string `db:"crl"`
|
||||||
|
}
|
|
@ -98,6 +98,11 @@ func Fingerprint256(data []byte) string {
|
||||||
// URLs that automatically marshal/unmarshal to JSON strings
|
// URLs that automatically marshal/unmarshal to JSON strings
|
||||||
type AcmeURL url.URL
|
type AcmeURL url.URL
|
||||||
|
|
||||||
|
func (u AcmeURL) String() string {
|
||||||
|
url := url.URL(u)
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (u AcmeURL) PathSegments() (segments []string) {
|
func (u AcmeURL) PathSegments() (segments []string) {
|
||||||
segments = strings.Split(u.Path, "/")
|
segments = strings.Split(u.Path, "/")
|
||||||
if len(segments) > 0 && len(segments[0]) == 0 {
|
if len(segments) > 0 && len(segments[0]) == 0 {
|
||||||
|
|
|
@ -47,9 +47,15 @@ func lastPathSegment(url core.AcmeURL) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration, key jose.JsonWebKey) (reg core.Registration, err error) {
|
func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration, key jose.JsonWebKey) (reg core.Registration, err error) {
|
||||||
|
// TODO: We should be able to just pass in a JsonWebKey. Blocked on migration
|
||||||
|
// to go-jose.
|
||||||
|
if !core.GoodKey(key.Rsa) {
|
||||||
|
return core.Registration{}, fmt.Errorf("Invalid public key.")
|
||||||
|
}
|
||||||
|
|
||||||
regID, err := ra.SA.NewRegistration()
|
regID, err := ra.SA.NewRegistration()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return core.Registration{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reg = core.Registration{
|
reg = core.Registration{
|
||||||
|
@ -61,7 +67,7 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration, key
|
||||||
|
|
||||||
// Store the authorization object, then return it
|
// Store the authorization object, then return it
|
||||||
err = ra.SA.UpdateRegistration(reg)
|
err = ra.SA.UpdateRegistration(reg)
|
||||||
return
|
return reg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization, key jose.JsonWebKey) (authz core.Authorization, err error) {
|
func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization, key jose.JsonWebKey) (authz core.Authorization, err error) {
|
||||||
|
|
|
@ -63,13 +63,21 @@ func (cadb *MockCADatabase) IncrementAndGetSerial() (int, error) {
|
||||||
var (
|
var (
|
||||||
// These values we simulate from the client
|
// These values we simulate from the client
|
||||||
AccountKeyJSON = []byte(`{
|
AccountKeyJSON = []byte(`{
|
||||||
"kty": "EC",
|
"e": "AQAB",
|
||||||
"crv": "P-521",
|
"kty": "RSA",
|
||||||
"x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
|
"n": "tSwgy3ORGvc7YJI9B2qqkelZRUC6F1S5NwXFvM4w5-M0TsxbFsH5UH6adigV0jzsDJ5imAechcSoOhAh9POceCbPN1sTNwLpNbOLiQQ7RD5mY_pSUHWXNmS9R4NZ3t2fQAzPeW7jOfF0LKuJRGkekx6tXP1uSnNibgpJULNc4208dgBaCHo3mvaE2HV2GmVl1yxwWX5QZZkGQGjNDZYnjFfa2DKVvFs0QbAk21ROm594kAxlRlMMrvqlf24Eq4ERO0ptzpZgm_3j_e4hGRD39gJS7kAzK-j2cacFQ5Qi2Y6wZI2p-FCq_wiYsfEAIkATPBiLKl_6d_Jfcvs_impcXQ"
|
||||||
"y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
|
}`)
|
||||||
}`)
|
|
||||||
AccountKey = jose.JsonWebKey{}
|
AccountKey = jose.JsonWebKey{}
|
||||||
|
|
||||||
|
ShortKeyJSON = []byte(`{
|
||||||
|
"e": "AQAB",
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "tSwgy3ORGvc7YJI9B2qqkelZRUC6F1S5NwXFvM4w5-M0TsxbFsH5UH6adigV0jzsDJ5imAechcSoOhAh9POceCbPN1sTNwLpNbOLiQQ7RD5mY_"
|
||||||
|
}`)
|
||||||
|
|
||||||
|
ShortKey = jose.JsonWebKey{}
|
||||||
|
|
||||||
AuthzRequest = core.Authorization{
|
AuthzRequest = core.Authorization{
|
||||||
Identifier: core.AcmeIdentifier{
|
Identifier: core.AcmeIdentifier{
|
||||||
Type: core.IdentifierDNS,
|
Type: core.IdentifierDNS,
|
||||||
|
@ -96,6 +104,9 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
|
||||||
err := json.Unmarshal(AccountKeyJSON, &AccountKey)
|
err := json.Unmarshal(AccountKeyJSON, &AccountKey)
|
||||||
test.AssertNotError(t, err, "Failed to unmarshall JWK")
|
test.AssertNotError(t, err, "Failed to unmarshall JWK")
|
||||||
|
|
||||||
|
err = json.Unmarshal(ShortKeyJSON, &ShortKey)
|
||||||
|
test.AssertNotError(t, err, "Failed to unmarshall JWK")
|
||||||
|
|
||||||
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
|
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
|
||||||
test.AssertNotError(t, err, "Failed to create SA")
|
test.AssertNotError(t, err, "Failed to create SA")
|
||||||
sa.InitTables()
|
sa.InitTables()
|
||||||
|
@ -131,6 +142,60 @@ func assertAuthzEqual(t *testing.T, a1, a2 core.Authorization) {
|
||||||
// Not testing: Contact, Challenges
|
// Not testing: Contact, Challenges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewRegistration(t *testing.T) {
|
||||||
|
_, _, sa, ra := initAuthorities(t)
|
||||||
|
mailto, _ := url.Parse("mailto:foo@bar.com")
|
||||||
|
input := core.Registration{
|
||||||
|
Contact: []core.AcmeURL{core.AcmeURL(*mailto)},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ra.NewRegistration(input, AccountKey)
|
||||||
|
test.AssertNotError(t, err, "Could not create new registration")
|
||||||
|
|
||||||
|
test.Assert(t, result.Key.Equals(AccountKey), "Key didn't match")
|
||||||
|
test.Assert(t, len(result.Contact) == 1, "Wrong number of contacts")
|
||||||
|
test.Assert(t, mailto.String() == result.Contact[0].String(),
|
||||||
|
"Contact didn't match")
|
||||||
|
test.Assert(t, result.Agreement == "", "Agreement didn't default empty")
|
||||||
|
test.Assert(t, result.RecoveryToken != "", "Recovery token not filled")
|
||||||
|
|
||||||
|
reg, err := sa.GetRegistration(result.ID)
|
||||||
|
test.AssertNotError(t, err, "Failed to retrieve registration")
|
||||||
|
test.Assert(t, reg.Key.Equals(AccountKey), "Retrieved registration differed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRegistrationNoFieldOverwrite(t *testing.T) {
|
||||||
|
_, _, _, ra := initAuthorities(t)
|
||||||
|
mailto, _ := url.Parse("mailto:foo@bar.com")
|
||||||
|
input := core.Registration{
|
||||||
|
ID: "hi",
|
||||||
|
Key: ShortKey,
|
||||||
|
RecoveryToken: "RecoverMe",
|
||||||
|
Contact: []core.AcmeURL{core.AcmeURL(*mailto)},
|
||||||
|
Agreement: "I agreed",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ra.NewRegistration(input, AccountKey)
|
||||||
|
test.AssertNotError(t, err, "Could not create new registration")
|
||||||
|
|
||||||
|
test.Assert(t, result.ID != "hi", "ID shouldn't be overwritten")
|
||||||
|
test.Assert(t, !result.Key.Equals(ShortKey), "Key shouldn't be overwritten")
|
||||||
|
// TODO: Enable this test case once we validate terms agreement.
|
||||||
|
// test.Assert(t, result.Agreement != "I agreed", "Agreement shouldn't be overwritten with invalid URL")
|
||||||
|
test.Assert(t, result.RecoveryToken != "RecoverMe", "Recovery token shouldn't be overwritten")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRegistrationBadKey(t *testing.T) {
|
||||||
|
_, _, _, ra := initAuthorities(t)
|
||||||
|
mailto, _ := url.Parse("mailto:foo@bar.com")
|
||||||
|
input := core.Registration{
|
||||||
|
Contact: []core.AcmeURL{core.AcmeURL(*mailto)},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ra.NewRegistration(input, ShortKey)
|
||||||
|
test.AssertError(t, err, "Should have rejected authorization with short key")
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewAuthorization(t *testing.T) {
|
func TestNewAuthorization(t *testing.T) {
|
||||||
_, _, sa, ra := initAuthorities(t)
|
_, _, sa, ra := initAuthorities(t)
|
||||||
|
|
||||||
|
|
|
@ -314,7 +314,6 @@ function getReadyToValidate(err, resp, body) {
|
||||||
var challenge = simpleHttps[0];
|
var challenge = simpleHttps[0];
|
||||||
var path = crypto.randomString(8) + ".txt";
|
var path = crypto.randomString(8) + ".txt";
|
||||||
var challengePath = ".well-known/acme-challenge/" + path;
|
var challengePath = ".well-known/acme-challenge/" + path;
|
||||||
fs.writeFileSync(challengePath, challenge.token);
|
|
||||||
state.responseURL = challenge["uri"];
|
state.responseURL = challenge["uri"];
|
||||||
state.path = path;
|
state.path = path;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue