forked from TrueCloudLab/certificates
Compare commits
37 commits
master
...
dcow/chall
Author | SHA1 | Date | |
---|---|---|---|
|
112fc59f46 | ||
|
6c39439008 | ||
|
05780554d2 | ||
|
d54f963b81 | ||
|
0f63e43b10 | ||
|
9103880f88 | ||
|
d5f95dee57 | ||
|
deacbdc358 | ||
|
f0228183f5 | ||
|
c378e0043a | ||
|
b8b3ca2ac1 | ||
|
5e5a76c3b5 | ||
|
5354906b9c | ||
|
976c8f82c6 | ||
|
b061d0af34 | ||
|
609e1312da | ||
|
8ae32f50f2 | ||
|
794725bcc3 | ||
|
8556d45c3f | ||
|
84af2ad562 | ||
|
2514b58f58 | ||
|
089e3aea4f | ||
|
9f18882973 | ||
|
a857c45847 | ||
|
2d0a00c4e1 | ||
|
8326632f5b | ||
|
9518ba44b1 | ||
|
bdadea8a37 | ||
|
9af4dd3692 | ||
|
5e6a020da5 | ||
|
f56c449ea4 | ||
|
8fb558da10 | ||
|
8d4356733e | ||
|
f9779d0bed | ||
|
66b2c4b1a4 | ||
|
40d7c42e33 | ||
|
6fdbd856f7 |
15 changed files with 1273 additions and 1004 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -19,3 +19,7 @@ coverage.txt
|
|||
vendor
|
||||
output
|
||||
.idea
|
||||
|
||||
# vscode
|
||||
*.code-workspace
|
||||
*_bin
|
||||
|
|
|
@ -106,26 +106,57 @@ func (h *Handler) GetAuthz(w http.ResponseWriter, r *http.Request) {
|
|||
api.JSON(w, authz)
|
||||
}
|
||||
|
||||
// GetChallenge ACME api for retrieving a Challenge.
|
||||
// GetChallenge is the ACME api for retrieving a Challenge resource.
|
||||
//
|
||||
// Potential Challenges are requested by the client when creating an order.
|
||||
// Once the client knows the appropriate validation resources are provisioned,
|
||||
// it makes a POST-as-GET request to this endpoint in order to initiate the
|
||||
// validation flow.
|
||||
//
|
||||
// The validation state machine describes the flow for a challenge.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc8555#section-7.1.6
|
||||
//
|
||||
// Once a validation attempt has completed without error, the challenge's
|
||||
// status is updated depending on the result (valid|invalid) of the server's
|
||||
// validation attempt. Once this is the case, a challenge cannot be reset.
|
||||
//
|
||||
// If a challenge cannot be completed because no suitable data can be
|
||||
// acquired the server (whilst communicating retry information) and the
|
||||
// client (whilst respecting the information from the server) may request
|
||||
// retries of the validation.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc8555#section-8.2
|
||||
//
|
||||
// Retry status is communicated using the error field and by sending a
|
||||
// Retry-After header back to the client.
|
||||
//
|
||||
// The request body is challenge-specific. The current challenges (http-01,
|
||||
// dns-01, tls-alpn-01) simply expect an empty object ("{}") in the payload
|
||||
// of the JWT sent by the client. We don't gain anything by stricly enforcing
|
||||
// nonexistence of unknown attributes, or, in these three cases, enforcing
|
||||
// an empty payload. And the spec also says to just ignore it:
|
||||
//
|
||||
// > The server MUST ignore any fields in the response object
|
||||
// > that are not specified as response fields for this type of challenge.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc8555#section-7.5.1
|
||||
//
|
||||
func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
acc, err := acme.AccountFromContext(r.Context())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
// Just verify that the payload was set, since we're not strictly adhering
|
||||
// to ACME V2 spec for reasons specified below.
|
||||
|
||||
// Just verify that the payload was set since the client is required
|
||||
// to send _something_.
|
||||
_, err = payloadFromContext(r.Context())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: We should be checking that the request is either a POST-as-GET, or
|
||||
// that the payload is an empty JSON block ({}). However, older ACME clients
|
||||
// still send a vestigial body (rather than an empty JSON block) and
|
||||
// strict enforcement would render these clients broken. For the time being
|
||||
// we'll just ignore the body.
|
||||
var (
|
||||
ch *acme.Challenge
|
||||
chID = chi.URLParam(r, "chID")
|
||||
|
@ -138,6 +169,13 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
w.Header().Add("Link", link(h.Auth.GetLink(r.Context(), acme.AuthzLink, true, ch.GetAuthzID()), "up"))
|
||||
w.Header().Set("Location", h.Auth.GetLink(r.Context(), acme.ChallengeLink, true, ch.GetID()))
|
||||
|
||||
if ch.Status == acme.StatusProcessing {
|
||||
w.Header().Add("Retry-After", ch.RetryAfter)
|
||||
// 200s are cachable. Don't cache this because it will likely change.
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
}
|
||||
|
||||
api.JSON(w, ch)
|
||||
}
|
||||
|
||||
|
|
|
@ -244,7 +244,7 @@ func TestHandlerGetNonce(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandlerGetDirectory(t *testing.T) {
|
||||
auth, err := acme.NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
||||
auth, err := acme.NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
prov := newProv()
|
||||
|
@ -599,6 +599,7 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
ch acme.Challenge
|
||||
problem *acme.Error
|
||||
}
|
||||
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-account": func(t *testing.T) test {
|
||||
return test{
|
||||
|
@ -607,6 +608,7 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/nil-account": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, acme.AccContextKey, nil)
|
||||
|
@ -616,6 +618,7 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/no-payload": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
|
||||
|
@ -626,6 +629,7 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/nil-payload": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
|
||||
|
@ -637,6 +641,7 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/validate-challenge-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
|
||||
|
@ -645,13 +650,14 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
err: acme.UnauthorizedErr(nil),
|
||||
err: acme.ServerInternalErr(nil),
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 401,
|
||||
problem: acme.UnauthorizedErr(nil),
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(nil),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/get-challenge-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
|
||||
|
@ -667,6 +673,7 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
problem: acme.UnauthorizedErr(nil),
|
||||
}
|
||||
},
|
||||
|
||||
"ok/validate-challenge": func(t *testing.T) test {
|
||||
key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -714,7 +721,60 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
ch: ch,
|
||||
}
|
||||
},
|
||||
|
||||
"ok/retry-after": func(t *testing.T) test {
|
||||
key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{ID: "accID", Key: key}
|
||||
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, acme.AccContextKey, acc)
|
||||
ctx = context.WithValue(ctx, acme.PayloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
|
||||
ch := ch()
|
||||
ch.Status = "processing"
|
||||
ch.RetryAfter = time.Now().Add(1 * time.Minute).UTC().Format(time.RFC3339)
|
||||
chJSON, err := json.Marshal(ch)
|
||||
assert.FatalError(t, err)
|
||||
ctx = context.WithValue(ctx, acme.PayloadContextKey, &payloadInfo{value: chJSON})
|
||||
count := 0
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
validateChallenge: func(ctx context.Context, accID, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) {
|
||||
p, err := acme.ProvisionerFromContext(ctx)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, accID, acc.ID)
|
||||
assert.Equals(t, id, ch.ID)
|
||||
assert.Equals(t, jwk.KeyID, key.KeyID)
|
||||
return &ch, nil
|
||||
},
|
||||
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
|
||||
var ret string
|
||||
switch count {
|
||||
case 0:
|
||||
assert.Equals(t, typ, acme.AuthzLink)
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{ch.AuthzID})
|
||||
ret = fmt.Sprintf("%s/acme/%s/authz/%s", baseURL.String(), provName, ch.AuthzID)
|
||||
case 1:
|
||||
assert.Equals(t, typ, acme.ChallengeLink)
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{ch.ID})
|
||||
ret = url
|
||||
}
|
||||
count++
|
||||
return ret
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
ch: ch,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Run the tests
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
@ -741,13 +801,22 @@ func TestHandlerGetChallenge(t *testing.T) {
|
|||
assert.Equals(t, ae.Identifier, prob.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
} else if res.StatusCode >= 200 {
|
||||
expB, err := json.Marshal(tc.ch)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, bytes.TrimSpace(body), expB)
|
||||
assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<%s/acme/%s/authz/%s>;rel=\"up\"", baseURL, provName, tc.ch.AuthzID)})
|
||||
assert.Equals(t, res.Header["Location"], []string{url})
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
switch tc.ch.Status {
|
||||
case "processing":
|
||||
assert.Equals(t, res.Header["Cache-Control"], []string{"no-cache"})
|
||||
assert.Equals(t, res.Header["Retry-After"], []string{tc.ch.RetryAfter})
|
||||
case "valid", "invalid":
|
||||
//
|
||||
}
|
||||
} else {
|
||||
assert.Fatal(t, false, "Unexpected Status Code")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -50,6 +51,7 @@ type Authority struct {
|
|||
db nosql.DB
|
||||
dir *directory
|
||||
signAuth SignAuthority
|
||||
ordinal int
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -64,7 +66,7 @@ var (
|
|||
)
|
||||
|
||||
// NewAuthority returns a new Authority that implements the ACME interface.
|
||||
func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) (*Authority, error) {
|
||||
func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority, ordinal int) (*Authority, error) {
|
||||
if _, ok := db.(*database.SimpleDB); !ok {
|
||||
// If it's not a SimpleDB then go ahead and bootstrap the DB with the
|
||||
// necessary ACME tables. SimpleDB should ONLY be used for testing.
|
||||
|
@ -79,7 +81,7 @@ func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) (*Aut
|
|||
}
|
||||
}
|
||||
return &Authority{
|
||||
db: db, dir: newDirectory(dns, prefix), signAuth: signAuth,
|
||||
db: db, dir: newDirectory(dns, prefix), signAuth: signAuth, ordinal: ordinal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -269,32 +271,225 @@ func (a *Authority) GetAuthz(ctx context.Context, accID, authzID string) (*Authz
|
|||
return az.toACME(ctx, a.db, a.dir)
|
||||
}
|
||||
|
||||
// ValidateChallenge attempts to validate the challenge.
|
||||
// ValidateChallenge loads a challenge resource and then begins the validation process if the challenge
|
||||
// is not in one of its terminal states {valid|invalid}.
|
||||
//
|
||||
// The challenge validation state machine looks like:
|
||||
//
|
||||
// * https://tools.ietf.org/html/rfc8555#section-7.1.6
|
||||
//
|
||||
// While in the processing state, the server may retry as it sees fit. The challenge validation strategy
|
||||
// needs to be rather specific in order for retries to work in a replicated, crash-proof deployment.
|
||||
// In general, the goal is to allow requests to hit arbitrary instances of step-ca while managing retry
|
||||
// responsibility such that multiple instances agree on an owner. Additionally, when a deployment of the
|
||||
// CA is in progress, the ownership should be carried forward and new, updated (or in general, restarted),
|
||||
// instances should pick back up where the crashed instance left off.
|
||||
//
|
||||
// The steps are:
|
||||
//
|
||||
// 1. Upon incoming request to the challenge endpoint, take ownership of the retry responsibility.
|
||||
// (a) Set Retry.Owner to this instance's ordinal (STEP_CA_ORDINAL).
|
||||
// (b) Set Retry.NumAttempts to 0 and Retry.MaxAttempts to the desired max.
|
||||
// (c) Set Challenge.Status to "processing"
|
||||
// (d) Set retry_after to a time (retryInterval) in the future.
|
||||
// 2. Perform the validation attempt.
|
||||
// 3. If the validation attempt results in a challenge that is still processing, schedule a retry.
|
||||
//
|
||||
// It's possible that another request to re-attempt the challenge comes in while a retry attempt is
|
||||
// pending from a previous request. In general, these old attempts will see that Retry.NextAttempt
|
||||
// is in the future and drop their task. Because another instance may have taken ownership, old attempts
|
||||
// would also see a different ordinal than their own.
|
||||
//
|
||||
// 4. When the retry timer fires, check to make sure the retry should still process.
|
||||
// (a) Refresh the challenge from the DB.
|
||||
// (a) Check that Retry.Owner is equal to this instance's ordinal.
|
||||
// (b) Check that Retry.NextAttempt is in the past.
|
||||
// 5. If the retry will commence, immediately update Retry.NextAttempt and save the challenge.
|
||||
//
|
||||
// Finally, if this instance is terminated, retries need to be reschedule when the instance restarts. This
|
||||
// is handled in the acme provisioner (authority/provisioner/acme.go) initialization.
|
||||
//
|
||||
// Note: the default ordinal does not need to be changed unless step-ca is running in a replicated scenario.
|
||||
//
|
||||
func (a *Authority) ValidateChallenge(ctx context.Context, accID, chID string, jwk *jose.JSONWebKey) (*Challenge, error) {
|
||||
ch, err := getChallenge(a.db, chID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch ch.getStatus() {
|
||||
case StatusPending, StatusProcessing:
|
||||
break
|
||||
case StatusInvalid, StatusValid:
|
||||
return ch.toACME(ctx, a.dir)
|
||||
default:
|
||||
e := errors.Errorf("unknown challenge state: %s", ch.getStatus())
|
||||
return nil, ServerInternalErr(e)
|
||||
}
|
||||
|
||||
// Validate the challenge belongs to the account owned by the requester.
|
||||
if accID != ch.getAccountID() {
|
||||
return nil, UnauthorizedErr(errors.New("account does not own challenge"))
|
||||
}
|
||||
|
||||
p, err := ProvisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Take ownership of the challenge status and retry state. The values must be reset.
|
||||
up := ch.clone()
|
||||
up.Status = StatusProcessing
|
||||
up.Retry = &Retry{
|
||||
Owner: a.ordinal,
|
||||
ProvisionerID: p.GetID(),
|
||||
NumAttempts: 0,
|
||||
MaxAttempts: 10,
|
||||
NextAttempt: time.Now().Add(retryInterval).UTC().Format(time.RFC3339),
|
||||
}
|
||||
err = up.save(a.db, ch)
|
||||
if err != nil {
|
||||
return nil, Wrap(err, "error saving challenge")
|
||||
}
|
||||
ch = up
|
||||
|
||||
v, err := a.validate(ch, jwk)
|
||||
// An error here is non-recoverable. Recoverable errors are set on the challenge object
|
||||
// and should not be returned directly.
|
||||
if err != nil {
|
||||
return nil, Wrap(err, "error attempting challenge validation")
|
||||
}
|
||||
err = v.save(a.db, ch)
|
||||
if err != nil {
|
||||
return nil, Wrap(err, "error saving challenge")
|
||||
}
|
||||
ch = v
|
||||
|
||||
switch ch.getStatus() {
|
||||
case StatusValid, StatusInvalid:
|
||||
break
|
||||
case StatusProcessing:
|
||||
if ch.getRetry().Active() {
|
||||
time.AfterFunc(retryInterval, func() {
|
||||
a.RetryChallenge(ch.getID())
|
||||
})
|
||||
}
|
||||
default:
|
||||
e := errors.Errorf("post-validation challenge in unexpected state, %s", ch.getStatus())
|
||||
return nil, ServerInternalErr(e)
|
||||
}
|
||||
return ch.toACME(ctx, a.dir)
|
||||
}
|
||||
|
||||
// The challenge validation process is specific to the type of challenge (dns-01, http-01, tls-alpn-01).
|
||||
// But, we still pass generic "options" to the polymorphic validate call.
|
||||
func (a *Authority) validate(ch challenge, jwk *jose.JSONWebKey) (challenge, error) {
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(30 * time.Second),
|
||||
}
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
ch, err = ch.validate(a.db, jwk, validateOptions{
|
||||
return ch.clone().morph().validate(jwk, validateOptions{
|
||||
httpGet: client.Get,
|
||||
lookupTxt: net.LookupTXT,
|
||||
tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
||||
return tls.DialWithDialer(dialer, network, addr, config)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const retryInterval = 12 * time.Second
|
||||
|
||||
// RetryChallenge behaves similar to ValidateChallenge, but simply attempts to perform a validation and
|
||||
// write update the challenge record in the db if the challenge has remaining retry attempts.
|
||||
//
|
||||
// see: ValidateChallenge
|
||||
func (a *Authority) RetryChallenge(chID string) {
|
||||
ch, err := getChallenge(a.db, chID)
|
||||
if err != nil {
|
||||
return nil, Wrap(err, "error attempting challenge validation")
|
||||
return
|
||||
}
|
||||
switch ch.getStatus() {
|
||||
case StatusPending:
|
||||
e := errors.New("pending challenges must first be moved to the processing state")
|
||||
log.Printf("%v", e)
|
||||
return
|
||||
case StatusInvalid, StatusValid:
|
||||
return
|
||||
case StatusProcessing:
|
||||
break
|
||||
default:
|
||||
e := errors.Errorf("unknown challenge state: %s", ch.getStatus())
|
||||
log.Printf("%v", e)
|
||||
return
|
||||
}
|
||||
|
||||
// When retrying, check to make sure the ordinal has not changed.
|
||||
// Make sure there are still retries left.
|
||||
// Then check to make sure Retry.NextAttempt is in the past.
|
||||
retry := ch.getRetry()
|
||||
switch {
|
||||
case retry.Owner != a.ordinal:
|
||||
return
|
||||
case !retry.Active():
|
||||
return
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, retry.NextAttempt)
|
||||
now := time.Now().UTC()
|
||||
switch {
|
||||
case err != nil:
|
||||
return
|
||||
case t.Before(now):
|
||||
return
|
||||
}
|
||||
|
||||
// Update the db so that other retries simply drop when their timer fires.
|
||||
up := ch.clone()
|
||||
up.Retry.NextAttempt = now.Add(retryInterval).UTC().Format(time.RFC3339)
|
||||
up.Retry.NumAttempts++
|
||||
err = up.save(a.db, ch)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch = up
|
||||
|
||||
p, err := a.LoadProvisionerByID(retry.ProvisionerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if p.GetType() != provisioner.TypeACME {
|
||||
log.Printf("%v", AccountDoesNotExistErr(errors.New("provisioner must be of type ACME")))
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), ProvisionerContextKey, p)
|
||||
acc, err := a.GetAccount(ctx, ch.getAccountID())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v, err := a.validate(ch, acc.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = v.save(a.db, ch)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch = v
|
||||
|
||||
switch ch.getStatus() {
|
||||
case StatusValid, StatusInvalid:
|
||||
break
|
||||
case StatusProcessing:
|
||||
if ch.getRetry().Active() {
|
||||
time.AfterFunc(retryInterval, func() {
|
||||
a.RetryChallenge(ch.getID())
|
||||
})
|
||||
}
|
||||
default:
|
||||
e := errors.Errorf("post-validation challenge in unexpected state, %s", ch.getStatus())
|
||||
log.Printf("%v", e)
|
||||
}
|
||||
return ch.toACME(ctx, a.db, a.dir)
|
||||
}
|
||||
|
||||
// GetCertificate retrieves the Certificate by ID.
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAuthorityGetLink(t *testing.T) {
|
||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
prov := newProv()
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
|
@ -76,7 +76,7 @@ func TestAuthorityGetLink(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthorityGetDirectory(t *testing.T) {
|
||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
prov := newProv()
|
||||
|
@ -154,7 +154,7 @@ func TestAuthorityNewNonce(t *testing.T) {
|
|||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -170,7 +170,7 @@ func TestAuthorityNewNonce(t *testing.T) {
|
|||
*res = string(key)
|
||||
return nil, true, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -209,7 +209,7 @@ func TestAuthorityUseNonce(t *testing.T) {
|
|||
MUpdate: func(tx *database.Tx) error {
|
||||
return errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -221,7 +221,7 @@ func TestAuthorityUseNonce(t *testing.T) {
|
|||
MUpdate: func(tx *database.Tx) error {
|
||||
return nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -267,7 +267,7 @@ func TestAuthorityNewAccount(t *testing.T) {
|
|||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -293,7 +293,7 @@ func TestAuthorityNewAccount(t *testing.T) {
|
|||
count++
|
||||
return nil, true, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -345,7 +345,7 @@ func TestAuthorityGetAccount(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -362,7 +362,7 @@ func TestAuthorityGetAccount(t *testing.T) {
|
|||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -414,7 +414,7 @@ func TestAuthorityGetAccountByKey(t *testing.T) {
|
|||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
jwk.Key = "foo"
|
||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -433,7 +433,7 @@ func TestAuthorityGetAccountByKey(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(kid))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -465,7 +465,7 @@ func TestAuthorityGetAccountByKey(t *testing.T) {
|
|||
count++
|
||||
return ret, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -521,7 +521,7 @@ func TestAuthorityGetOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -540,7 +540,7 @@ func TestAuthorityGetOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(o.ID))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -569,7 +569,7 @@ func TestAuthorityGetOrder(t *testing.T) {
|
|||
return nil, ServerInternalErr(errors.New("force"))
|
||||
}
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -590,7 +590,7 @@ func TestAuthorityGetOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(o.ID))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -644,7 +644,7 @@ func TestAuthorityGetCertificate(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -663,7 +663,7 @@ func TestAuthorityGetCertificate(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(cert.ID))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -683,7 +683,7 @@ func TestAuthorityGetCertificate(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(cert.ID))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -740,7 +740,7 @@ func TestAuthorityGetAuthz(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -759,7 +759,7 @@ func TestAuthorityGetAuthz(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(az.getID()))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -790,7 +790,7 @@ func TestAuthorityGetAuthz(t *testing.T) {
|
|||
count++
|
||||
return ret, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -882,7 +882,7 @@ func TestAuthorityGetAuthz(t *testing.T) {
|
|||
count++
|
||||
return ret, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -934,7 +934,7 @@ func TestAuthorityNewOrder(t *testing.T) {
|
|||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -988,7 +988,7 @@ func TestAuthorityNewOrder(t *testing.T) {
|
|||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
return nil, database.ErrNotFound
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1042,7 +1042,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1074,7 +1074,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) {
|
|||
count++
|
||||
return ret, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1121,7 +1121,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) {
|
|||
count++
|
||||
return ret, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1172,7 +1172,7 @@ func TestAuthorityFinalizeOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1191,7 +1191,7 @@ func TestAuthorityFinalizeOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(o.ID))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1217,7 +1217,7 @@ func TestAuthorityFinalizeOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(o.ID))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1239,7 +1239,7 @@ func TestAuthorityFinalizeOrder(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(o.ID))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1296,7 +1296,7 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1304,6 +1304,7 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
err: ServerInternalErr(errors.Errorf("error loading challenge %s: force", id)),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/challenge-not-owned-by-account": func(t *testing.T) test {
|
||||
ch, err := newHTTPCh()
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1315,7 +1316,7 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(ch.getID()))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1324,6 +1325,7 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
err: UnauthorizedErr(errors.New("account does not own challenge")),
|
||||
}
|
||||
},
|
||||
|
||||
"fail/validate-error": func(t *testing.T) test {
|
||||
ch, err := newHTTPCh()
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1340,23 +1342,25 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(ch.getID()))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
id: ch.getID(),
|
||||
accID: ch.getAccountID(),
|
||||
err: ServerInternalErr(errors.New("error attempting challenge validation: error saving acme challenge: force")),
|
||||
err: ServerInternalErr(errors.New("error saving challenge: error saving acme challenge: force")),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
|
||||
"ok/already-valid": func(t *testing.T) test {
|
||||
ch, err := newHTTPCh()
|
||||
assert.FatalError(t, err)
|
||||
_ch, ok := ch.(*http01Challenge)
|
||||
assert.Fatal(t, ok)
|
||||
_ch.baseChallenge.Status = StatusValid
|
||||
_ch.baseChallenge.Validated = clock.Now()
|
||||
b, err := json.Marshal(ch)
|
||||
bc := ch.clone()
|
||||
bc.Status = StatusValid
|
||||
bc.Validated = clock.Now()
|
||||
bc.Retry = nil
|
||||
rch := bc.morph()
|
||||
b, err := json.Marshal(rch)
|
||||
assert.FatalError(t, err)
|
||||
auth, err := NewAuthority(&db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
|
@ -1364,16 +1368,17 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(ch.getID()))
|
||||
return b, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
id: ch.getID(),
|
||||
accID: ch.getAccountID(),
|
||||
ch: ch,
|
||||
ch: rch,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
|
@ -1389,12 +1394,10 @@ func TestAuthorityValidateChallenge(t *testing.T) {
|
|||
if assert.Nil(t, tc.err) {
|
||||
gotb, err := json.Marshal(acmeCh)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
acmeExp, err := tc.ch.toACME(ctx, nil, tc.auth.dir)
|
||||
acmeExp, err := tc.ch.toACME(ctx, tc.auth.dir)
|
||||
assert.FatalError(t, err)
|
||||
expb, err := json.Marshal(acmeExp)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
assert.Equals(t, expb, gotb)
|
||||
}
|
||||
}
|
||||
|
@ -1423,7 +1426,7 @@ func TestAuthorityUpdateAccount(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1445,7 +1448,7 @@ func TestAuthorityUpdateAccount(t *testing.T) {
|
|||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1473,7 +1476,7 @@ func TestAuthorityUpdateAccount(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(acc.ID))
|
||||
return nil, true, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1530,7 +1533,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(id))
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1551,7 +1554,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) {
|
|||
MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
@ -1579,7 +1582,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) {
|
|||
assert.Equals(t, key, []byte(acc.ID))
|
||||
return nil, true, nil
|
||||
},
|
||||
}, "ca.smallstep.com", "acme", nil)
|
||||
}, "ca.smallstep.com", "acme", nil, 0)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
auth: auth,
|
||||
|
|
|
@ -148,7 +148,7 @@ func (ba *baseAuthz) toACME(ctx context.Context, db nosql.DB, dir *directory) (*
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chs[i], err = ch.toACME(ctx, db, dir)
|
||||
chs[i], err = ch.toACME(ctx, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -438,9 +438,9 @@ func TestAuthzToACME(t *testing.T) {
|
|||
assert.Equals(t, acmeAz.Identifier, iden)
|
||||
assert.Equals(t, acmeAz.Status, StatusPending)
|
||||
|
||||
acmeCh1, err := ch1.toACME(ctx, nil, dir)
|
||||
acmeCh1, err := ch1.toACME(ctx, dir)
|
||||
assert.FatalError(t, err)
|
||||
acmeCh2, err := ch2.toACME(ctx, nil, dir)
|
||||
acmeCh2, err := ch2.toACME(ctx, dir)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
assert.Equals(t, acmeAz.Challenges[0], acmeCh1)
|
||||
|
|
|
@ -25,14 +25,15 @@ import (
|
|||
// Challenge is a subset of the challenge type containing only those attributes
|
||||
// required for responses in the ACME protocol.
|
||||
type Challenge struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Validated string `json:"validated,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Error *AError `json:"error,omitempty"`
|
||||
ID string `json:"-"`
|
||||
AuthzID string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Validated string `json:"validated,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Error *AError `json:"error,omitempty"`
|
||||
RetryAfter string `json:"retry_after,omitempty"`
|
||||
ID string `json:"-"`
|
||||
AuthzID string `json:"-"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
|
@ -67,7 +68,7 @@ type validateOptions struct {
|
|||
// challenge is the interface ACME challenege types must implement.
|
||||
type challenge interface {
|
||||
save(db nosql.DB, swap challenge) error
|
||||
validate(nosql.DB, *jose.JSONWebKey, validateOptions) (challenge, error)
|
||||
validate(*jose.JSONWebKey, validateOptions) (challenge, error)
|
||||
getType() string
|
||||
getError() *AError
|
||||
getValue() string
|
||||
|
@ -75,18 +76,20 @@ type challenge interface {
|
|||
getID() string
|
||||
getAuthzID() string
|
||||
getToken() string
|
||||
getRetry() *Retry
|
||||
clone() *baseChallenge
|
||||
getAccountID() string
|
||||
getValidated() time.Time
|
||||
getCreated() time.Time
|
||||
toACME(context.Context, nosql.DB, *directory) (*Challenge, error)
|
||||
toACME(context.Context, *directory) (*Challenge, error)
|
||||
}
|
||||
|
||||
// ChallengeOptions is the type used to created a new Challenge.
|
||||
type ChallengeOptions struct {
|
||||
AccountID string
|
||||
AuthzID string
|
||||
Identifier Identifier
|
||||
AccountID string
|
||||
AuthzID string
|
||||
ProvisionerID string
|
||||
Identifier Identifier
|
||||
}
|
||||
|
||||
// baseChallenge is the base Challenge type that others build from.
|
||||
|
@ -98,9 +101,10 @@ type baseChallenge struct {
|
|||
Status string `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Value string `json:"value"`
|
||||
Validated time.Time `json:"validated"`
|
||||
Created time.Time `json:"created"`
|
||||
Validated time.Time `json:"validated"`
|
||||
Error *AError `json:"error"`
|
||||
Retry *Retry `json:"retry"`
|
||||
}
|
||||
|
||||
func newBaseChallenge(accountID, authzID string) (*baseChallenge, error) {
|
||||
|
@ -158,6 +162,11 @@ func (bc *baseChallenge) getToken() string {
|
|||
return bc.Token
|
||||
}
|
||||
|
||||
// getRetry returns the retry state of the baseChallenge
|
||||
func (bc *baseChallenge) getRetry() *Retry {
|
||||
return bc.Retry
|
||||
}
|
||||
|
||||
// getValidated returns the validated time of the baseChallenge.
|
||||
func (bc *baseChallenge) getValidated() time.Time {
|
||||
return bc.Validated
|
||||
|
@ -175,7 +184,7 @@ func (bc *baseChallenge) getError() *AError {
|
|||
|
||||
// toACME converts the internal Challenge type into the public acmeChallenge
|
||||
// type for presentation in the ACME protocol.
|
||||
func (bc *baseChallenge) toACME(ctx context.Context, db nosql.DB, dir *directory) (*Challenge, error) {
|
||||
func (bc *baseChallenge) toACME(ctx context.Context, dir *directory) (*Challenge, error) {
|
||||
ac := &Challenge{
|
||||
Type: bc.getType(),
|
||||
Status: bc.getStatus(),
|
||||
|
@ -190,6 +199,9 @@ func (bc *baseChallenge) toACME(ctx context.Context, db nosql.DB, dir *directory
|
|||
if bc.Error != nil {
|
||||
ac.Error = bc.Error
|
||||
}
|
||||
if bc.Retry != nil && bc.Status == StatusProcessing {
|
||||
ac.RetryAfter = bc.Retry.NextAttempt
|
||||
}
|
||||
return ac, nil
|
||||
}
|
||||
|
||||
|
@ -228,19 +240,17 @@ func (bc *baseChallenge) save(db nosql.DB, old challenge) error {
|
|||
|
||||
func (bc *baseChallenge) clone() *baseChallenge {
|
||||
u := *bc
|
||||
if bc.Retry != nil {
|
||||
r := *bc.Retry
|
||||
u.Retry = &r
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func (bc *baseChallenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
func (bc *baseChallenge) validate(jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
return nil, ServerInternalErr(errors.New("unimplemented"))
|
||||
}
|
||||
|
||||
func (bc *baseChallenge) storeError(db nosql.DB, err *Error) error {
|
||||
clone := bc.clone()
|
||||
clone.Error = err.ToACME()
|
||||
return clone.save(db, bc)
|
||||
}
|
||||
|
||||
// unmarshalChallenge unmarshals a challenge type into the correct sub-type.
|
||||
func unmarshalChallenge(data []byte) (challenge, error) {
|
||||
var getType struct {
|
||||
|
@ -277,6 +287,34 @@ func unmarshalChallenge(data []byte) (challenge, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bc *baseChallenge) morph() challenge {
|
||||
switch bc.getType() {
|
||||
case "dns-01":
|
||||
return &dns01Challenge{bc}
|
||||
case "http-01":
|
||||
return &http01Challenge{bc}
|
||||
case "tls-alpn-01":
|
||||
return &tlsALPN01Challenge{bc}
|
||||
default:
|
||||
return bc
|
||||
}
|
||||
}
|
||||
|
||||
// Retry information for challenges is internally relevant and needs to be stored in the DB, but should not be part
|
||||
// of the public challenge API apart from the Retry-After header.
|
||||
type Retry struct {
|
||||
Owner int `json:"owner"`
|
||||
ProvisionerID string `json:"provisionerid"`
|
||||
NumAttempts int `json:"numattempts"`
|
||||
MaxAttempts int `json:"maxattempts"`
|
||||
NextAttempt string `json:"nextattempt"`
|
||||
}
|
||||
|
||||
// Active returns a boolean indicating whether a Retry struct has remaining attempts or not.
|
||||
func (r *Retry) Active() bool {
|
||||
return r.NumAttempts < r.MaxAttempts
|
||||
}
|
||||
|
||||
// http01Challenge represents an http-01 acme challenge.
|
||||
type http01Challenge struct {
|
||||
*baseChallenge
|
||||
|
@ -301,61 +339,66 @@ func newHTTP01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error) {
|
|||
// Validate attempts to validate the challenge. If the challenge has been
|
||||
// satisfactorily validated, the 'status' and 'validated' attributes are
|
||||
// updated.
|
||||
func (hc *http01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
func (hc *http01Challenge) validate(jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
// If already valid or invalid then return without performing validation.
|
||||
if hc.getStatus() == StatusValid || hc.getStatus() == StatusInvalid {
|
||||
switch hc.getStatus() {
|
||||
case StatusPending:
|
||||
e := errors.New("pending challenges must first be moved to the processing state")
|
||||
return nil, ServerInternalErr(e)
|
||||
case StatusProcessing:
|
||||
break
|
||||
case StatusValid, StatusInvalid:
|
||||
return hc, nil
|
||||
default:
|
||||
e := errors.Errorf("unknown challenge state: %s", hc.getStatus())
|
||||
return nil, ServerInternalErr(e)
|
||||
}
|
||||
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", hc.Value, hc.Token)
|
||||
|
||||
up := &http01Challenge{hc.baseChallenge.clone()}
|
||||
|
||||
url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", hc.Value, hc.Token)
|
||||
resp, err := vo.httpGet(url)
|
||||
if err != nil {
|
||||
if err = hc.storeError(db, ConnectionErr(errors.Wrapf(err,
|
||||
"error doing http GET for url %s", url))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hc, nil
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if err = hc.storeError(db,
|
||||
ConnectionErr(errors.Errorf("error doing http GET for url %s with status code %d",
|
||||
url, resp.StatusCode))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hc, nil
|
||||
e := errors.Wrapf(err, "error doing http GET for url %s", url)
|
||||
up.Error = ConnectionErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
e := errors.Errorf("error doing http GET for url %s with status code %d", url, resp.StatusCode)
|
||||
up.Error = ConnectionErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, ServerInternalErr(errors.Wrapf(err, "error reading "+
|
||||
"response body for url %s", url))
|
||||
e := errors.Wrapf(err, "error reading response body for url %s", url)
|
||||
up.Error = ServerInternalErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
keyAuth := strings.Trim(string(body), "\r\n")
|
||||
|
||||
keyAuth := strings.Trim(string(body), "\r\n")
|
||||
expected, err := KeyAuthorization(hc.Token, jwk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if keyAuth != expected {
|
||||
if err = hc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("keyAuthorization does not match; "+
|
||||
"expected %s, but got %s", expected, keyAuth))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hc, nil
|
||||
|
||||
// success
|
||||
if keyAuth == expected {
|
||||
up.Validated = clock.Now()
|
||||
up.Status = StatusValid
|
||||
up.Error = nil
|
||||
up.Retry = nil
|
||||
return up, nil
|
||||
}
|
||||
|
||||
// Update and store the challenge.
|
||||
upd := &http01Challenge{hc.baseChallenge.clone()}
|
||||
upd.Status = StatusValid
|
||||
upd.Error = nil
|
||||
upd.Validated = clock.Now()
|
||||
|
||||
if err := upd.save(db, hc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return upd, nil
|
||||
// fail
|
||||
up.Status = StatusInvalid
|
||||
e := errors.Errorf("keyAuthorization does not match; expected %s, but got %s", expected, keyAuth)
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
up.Retry = nil
|
||||
return up, nil
|
||||
}
|
||||
|
||||
type tlsALPN01Challenge struct {
|
||||
|
@ -371,34 +414,41 @@ func newTLSALPN01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error)
|
|||
bc.Type = "tls-alpn-01"
|
||||
bc.Value = ops.Identifier.Value
|
||||
|
||||
hc := &tlsALPN01Challenge{bc}
|
||||
if err := hc.save(db, nil); err != nil {
|
||||
tc := &tlsALPN01Challenge{bc}
|
||||
if err := tc.save(db, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hc, nil
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
func (tc *tlsALPN01Challenge) validate(jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
// If already valid or invalid then return without performing validation.
|
||||
if tc.getStatus() == StatusValid || tc.getStatus() == StatusInvalid {
|
||||
switch tc.getStatus() {
|
||||
case StatusPending:
|
||||
e := errors.New("pending challenges must first be moved to the processing state")
|
||||
return nil, ServerInternalErr(e)
|
||||
case StatusProcessing:
|
||||
break
|
||||
case StatusValid, StatusInvalid:
|
||||
return tc, nil
|
||||
default:
|
||||
e := errors.Errorf("unknown challenge state: %s", tc.getStatus())
|
||||
return nil, ServerInternalErr(e)
|
||||
}
|
||||
|
||||
up := &tlsALPN01Challenge{tc.baseChallenge.clone()}
|
||||
|
||||
config := &tls.Config{
|
||||
NextProtos: []string{"acme-tls/1"},
|
||||
ServerName: tc.Value,
|
||||
InsecureSkipVerify: true, // we expect a self-signed challenge certificate
|
||||
}
|
||||
|
||||
hostPort := net.JoinHostPort(tc.Value, "443")
|
||||
|
||||
conn, err := vo.tlsDial("tcp", hostPort, config)
|
||||
if err != nil {
|
||||
if err = tc.storeError(db,
|
||||
ConnectionErr(errors.Wrapf(err, "error doing TLS dial for %s", hostPort))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Wrapf(err, "error doing TLS dial for %s", hostPort)
|
||||
up.Error = ConnectionErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
|
@ -406,32 +456,22 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val
|
|||
certs := cs.PeerCertificates
|
||||
|
||||
if len(certs) == 0 {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("%s challenge for %s resulted in no certificates",
|
||||
tc.Type, tc.Value))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("%s challenge for %s resulted in no certificates", tc.Type, tc.Value)
|
||||
up.Error = TLSErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != "acme-tls/1" {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("cannot negotiate ALPN acme-tls/1 protocol for "+
|
||||
"tls-alpn-01 challenge"))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge")
|
||||
up.Error = TLSErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
leafCert := certs[0]
|
||||
|
||||
if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], tc.Value) {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"leaf certificate must contain a single DNS name, %v", tc.Value))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"leaf certificate must contain a single DNS name, %v", tc.Value)
|
||||
up.Error = TLSErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
idPeAcmeIdentifier := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
||||
|
@ -447,45 +487,37 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val
|
|||
for _, ext := range leafCert.Extensions {
|
||||
if idPeAcmeIdentifier.Equal(ext.Id) {
|
||||
if !ext.Critical {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"acmeValidationV1 extension not critical"))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("incorrect certificate for tls-alpn-01 challenge: " +
|
||||
"acmeValidationV1 extension not critical")
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
var extValue []byte
|
||||
rest, err := asn1.Unmarshal(ext.Value, &extValue)
|
||||
|
||||
if err != nil || len(rest) > 0 || len(hashedKeyAuth) != len(extValue) {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"malformed acmeValidationV1 extension value"))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("incorrect certificate for tls-alpn-01 challenge: " +
|
||||
"malformed acmeValidationV1 extension value")
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(hashedKeyAuth[:], extValue) != 1 {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"expected acmeValidationV1 extension value %s for this challenge but got %s",
|
||||
hex.EncodeToString(hashedKeyAuth[:]), hex.EncodeToString(extValue)))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"expected acmeValidationV1 extension value %s for this challenge but got %s",
|
||||
hex.EncodeToString(hashedKeyAuth[:]), hex.EncodeToString(extValue))
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
// There is an appropriate value, but it doesn't match.
|
||||
up.Status = StatusInvalid
|
||||
return up, nil
|
||||
}
|
||||
|
||||
upd := &tlsALPN01Challenge{tc.baseChallenge.clone()}
|
||||
upd.Status = StatusValid
|
||||
upd.Error = nil
|
||||
upd.Validated = clock.Now()
|
||||
|
||||
if err := upd.save(db, tc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return upd, nil
|
||||
up.Validated = clock.Now()
|
||||
up.Status = StatusValid
|
||||
up.Error = nil
|
||||
up.Retry = nil
|
||||
return up, nil
|
||||
}
|
||||
|
||||
if idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) {
|
||||
|
@ -494,20 +526,16 @@ func (tc *tlsALPN01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo val
|
|||
}
|
||||
|
||||
if foundIDPeAcmeIdentifierV1Obsolete {
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension"))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("incorrect certificate for tls-alpn-01 challenge: " +
|
||||
"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension")
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
if err = tc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("incorrect certificate for tls-alpn-01 challenge: "+
|
||||
"missing acmeValidationV1 extension"))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
e := errors.Errorf("incorrect certificate for tls-alpn-01 challenge: " +
|
||||
"missing acmeValidationV1 extension")
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
// dns01Challenge represents an dns-01 acme challenge.
|
||||
|
@ -531,6 +559,70 @@ func newDNS01Challenge(db nosql.DB, ops ChallengeOptions) (challenge, error) {
|
|||
return dc, nil
|
||||
}
|
||||
|
||||
// validate attempts to validate the challenge. If the challenge has been
|
||||
// satisfactorily validated, the 'status' and 'validated' attributes are
|
||||
// updated.
|
||||
func (dc *dns01Challenge) validate(jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
// If already valid or invalid then return without performing validation.
|
||||
switch dc.getStatus() {
|
||||
case StatusPending:
|
||||
e := errors.New("pending challenges must first be moved to the processing state")
|
||||
return nil, ServerInternalErr(e)
|
||||
case StatusProcessing:
|
||||
break
|
||||
case StatusValid, StatusInvalid:
|
||||
return dc, nil
|
||||
default:
|
||||
e := errors.Errorf("unknown challenge state: %s", dc.getStatus())
|
||||
return nil, ServerInternalErr(e)
|
||||
}
|
||||
|
||||
up := &dns01Challenge{dc.baseChallenge.clone()}
|
||||
|
||||
// Normalize domain for wildcard DNS names
|
||||
// This is done to avoid making TXT lookups for domains like
|
||||
// _acme-challenge.*.example.com
|
||||
// Instead perform txt lookup for _acme-challenge.example.com
|
||||
domain := strings.TrimPrefix(dc.Value, "*.")
|
||||
record := "_acme-challenge." + domain
|
||||
|
||||
txtRecords, err := vo.lookupTxt(record)
|
||||
if err != nil {
|
||||
e := errors.Wrapf(err, "error looking up TXT records for domain %s", domain)
|
||||
up.Error = DNSErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
expectedKeyAuth, err := KeyAuthorization(dc.Token, jwk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := sha256.Sum256([]byte(expectedKeyAuth))
|
||||
expected := base64.RawURLEncoding.EncodeToString(h[:])
|
||||
|
||||
if len(txtRecords) == 0 {
|
||||
e := errors.Errorf("no TXT record found at '%s'", record)
|
||||
up.Error = DNSErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
for _, r := range txtRecords {
|
||||
if r == expected {
|
||||
up.Validated = clock.Now()
|
||||
up.Status = StatusValid
|
||||
up.Error = nil
|
||||
up.Retry = nil
|
||||
return up, nil
|
||||
}
|
||||
}
|
||||
|
||||
up.Status = StatusInvalid
|
||||
e := errors.Errorf("keyAuthorization does not match; expected %s, but got %s",
|
||||
expectedKeyAuth, txtRecords)
|
||||
up.Error = IncorrectResponseErr(e).ToACME()
|
||||
return up, nil
|
||||
}
|
||||
|
||||
// KeyAuthorization creates the ACME key authorization value from a token
|
||||
// and a jwk.
|
||||
func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) {
|
||||
|
@ -542,65 +634,6 @@ func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) {
|
|||
return fmt.Sprintf("%s.%s", token, encPrint), nil
|
||||
}
|
||||
|
||||
// validate attempts to validate the challenge. If the challenge has been
|
||||
// satisfactorily validated, the 'status' and 'validated' attributes are
|
||||
// updated.
|
||||
func (dc *dns01Challenge) validate(db nosql.DB, jwk *jose.JSONWebKey, vo validateOptions) (challenge, error) {
|
||||
// If already valid or invalid then return without performing validation.
|
||||
if dc.getStatus() == StatusValid || dc.getStatus() == StatusInvalid {
|
||||
return dc, nil
|
||||
}
|
||||
|
||||
// Normalize domain for wildcard DNS names
|
||||
// This is done to avoid making TXT lookups for domains like
|
||||
// _acme-challenge.*.example.com
|
||||
// Instead perform txt lookup for _acme-challenge.example.com
|
||||
domain := strings.TrimPrefix(dc.Value, "*.")
|
||||
|
||||
txtRecords, err := vo.lookupTxt("_acme-challenge." + domain)
|
||||
if err != nil {
|
||||
if err = dc.storeError(db,
|
||||
DNSErr(errors.Wrapf(err, "error looking up TXT "+
|
||||
"records for domain %s", domain))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dc, nil
|
||||
}
|
||||
|
||||
expectedKeyAuth, err := KeyAuthorization(dc.Token, jwk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := sha256.Sum256([]byte(expectedKeyAuth))
|
||||
expected := base64.RawURLEncoding.EncodeToString(h[:])
|
||||
var found bool
|
||||
for _, r := range txtRecords {
|
||||
if r == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if err = dc.storeError(db,
|
||||
RejectedIdentifierErr(errors.Errorf("keyAuthorization "+
|
||||
"does not match; expected %s, but got %s", expectedKeyAuth, txtRecords))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dc, nil
|
||||
}
|
||||
|
||||
// Update and store the challenge.
|
||||
upd := &dns01Challenge{dc.baseChallenge.clone()}
|
||||
upd.Status = StatusValid
|
||||
upd.Error = nil
|
||||
upd.Validated = time.Now().UTC()
|
||||
|
||||
if err := upd.save(db, dc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return upd, nil
|
||||
}
|
||||
|
||||
// getChallenge retrieves and unmarshals an ACME challenge type from the database.
|
||||
func getChallenge(db nosql.DB, id string) (challenge, error) {
|
||||
b, err := db.Get(challengeTable, []byte(id))
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -97,6 +97,8 @@ var (
|
|||
StatusInvalid = "invalid"
|
||||
// StatusPending -- pending; e.g. an Order that is not ready to be finalized.
|
||||
StatusPending = "pending"
|
||||
// StatusProcessing -- processing e.g. a Challenge that is in the process of being validated.
|
||||
StatusProcessing = "processing"
|
||||
// StatusDeactivated -- deactivated; e.g. for an Account that is not longer valid.
|
||||
StatusDeactivated = "deactivated"
|
||||
// StatusReady -- ready; e.g. for an Order that is ready to be finalized.
|
||||
|
|
|
@ -58,6 +58,8 @@ func (p *ACME) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// TODO: https://github.com/smallstep/certificates/issues/250
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
10
ca/ca.go
10
ca/ca.go
|
@ -26,6 +26,7 @@ type options struct {
|
|||
configFile string
|
||||
password []byte
|
||||
database db.AuthDB
|
||||
ordinal int
|
||||
}
|
||||
|
||||
func (o *options) apply(opts []Option) {
|
||||
|
@ -60,6 +61,13 @@ func WithDatabase(db db.AuthDB) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithOrdinal sets the server's ordinal identifier (an int).
|
||||
func WithOrdinal(ordinal int) Option {
|
||||
return func(o *options) {
|
||||
o.ordinal = ordinal
|
||||
}
|
||||
}
|
||||
|
||||
// CA is the type used to build the complete certificate authority. It builds
|
||||
// the HTTP server, set ups the middlewares and the HTTP handlers.
|
||||
type CA struct {
|
||||
|
@ -124,7 +132,7 @@ func (ca *CA) Init(config *authority.Config) (*CA, error) {
|
|||
}
|
||||
|
||||
prefix := "acme"
|
||||
acmeAuth, err := acme.NewAuthority(auth.GetDatabase().(nosql.DB), dns, prefix, auth)
|
||||
acmeAuth, err := acme.NewAuthority(auth.GetDatabase().(nosql.DB), dns, prefix, auth, ca.opts.ordinal)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating ACME authority")
|
||||
}
|
||||
|
|
|
@ -34,6 +34,11 @@ intermediate private key.`,
|
|||
Name: "resolver",
|
||||
Usage: "address of a DNS resolver to be used instead of the default.",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ordinal",
|
||||
Usage: `Unique <index> identifying this instance of step-ca in a highly-
|
||||
available (replicated) deployment.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -42,6 +47,9 @@ func appAction(ctx *cli.Context) error {
|
|||
passFile := ctx.String("password-file")
|
||||
resolver := ctx.String("resolver")
|
||||
|
||||
// grab the ordinal or default to 0
|
||||
ordinal := ctx.Int("ordinal")
|
||||
|
||||
// If zero cmd line args show help, if >1 cmd line args show error.
|
||||
if ctx.NArg() == 0 {
|
||||
return cli.ShowAppHelp(ctx)
|
||||
|
@ -72,7 +80,7 @@ func appAction(ctx *cli.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
srv, err := ca.New(config, ca.WithConfigFile(configFile), ca.WithPassword(password))
|
||||
srv, err := ca.New(config, ca.WithConfigFile(configFile), ca.WithPassword(password), ca.WithOrdinal(ordinal))
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
|
7
go.mod
7
go.mod
|
@ -8,21 +8,26 @@ require (
|
|||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/googleapis/gax-go/v2 v2.0.5
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/newrelic/go-agent v2.15.0+incompatible
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/rs/xid v1.2.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15
|
||||
github.com/smallstep/cli v0.14.3
|
||||
github.com/smallstep/nosql v0.3.0
|
||||
github.com/stretchr/testify v1.5.1 // indirect
|
||||
github.com/urfave/cli v1.22.2
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
|
||||
google.golang.org/api v0.15.0
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb
|
||||
google.golang.org/grpc v1.26.0
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.4.0
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
)
|
||||
|
||||
//replace github.com/smallstep/cli => ../cli
|
||||
|
|
13
go.sum
13
go.sum
|
@ -86,6 +86,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -300,6 +301,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible h1:GfzE+uq7odDW7nOmp1QWuilLEK7kJf8i84XcIfk3mKA=
|
||||
github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34=
|
||||
|
@ -362,6 +365,8 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1
|
|||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU=
|
||||
github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
|
@ -498,6 +503,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
|
@ -616,6 +623,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -749,6 +758,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
|
@ -770,6 +781,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
Loading…
Reference in a new issue