-
Notifications
You must be signed in to change notification settings - Fork 468
ACME (RFC 8555) § 8.2 Challenge Retries #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 30 commits
6fdbd85
40d7c42
66b2c4b
f9779d0
8d43567
8fb558d
f56c449
5e6a020
9af4dd3
bdadea8
9518ba4
8326632
2d0a00c
a857c45
9f18882
089e3ae
2514b58
84af2ad
8556d45
794725b
8ae32f5
609e131
b061d0a
976c8f8
5354906
5e5a76c
b8b3ca2
c378e00
f022818
deacbdc
d5f95de
9103880
0f63e43
d54f963
0578055
6c39439
112fc59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,7 @@ coverage.txt | |
vendor | ||
output | ||
.idea | ||
|
||
# vscode | ||
*.code-workspace | ||
*_bin |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -231,7 +231,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() | ||
url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/directory", acme.URLSafeProvisionerName(prov)) | ||
|
@@ -605,6 +605,7 @@ func TestHandlerGetChallenge(t *testing.T) { | |
ch acme.Challenge | ||
problem *acme.Error | ||
} | ||
|
||
var tests = map[string]func(t *testing.T) test{ | ||
"fail/no-provisioner": func(t *testing.T) test { | ||
return test{ | ||
|
@@ -613,20 +614,23 @@ func TestHandlerGetChallenge(t *testing.T) { | |
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")), | ||
} | ||
}, | ||
|
||
"fail/nil-provisioner": func(t *testing.T) test { | ||
return test{ | ||
ctx: context.WithValue(context.Background(), provisionerContextKey, nil), | ||
statusCode: 500, | ||
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")), | ||
} | ||
}, | ||
|
||
"fail/no-account": func(t *testing.T) test { | ||
return test{ | ||
ctx: context.WithValue(context.Background(), provisionerContextKey, prov), | ||
statusCode: 400, | ||
problem: acme.AccountDoesNotExistErr(nil), | ||
} | ||
}, | ||
|
||
"fail/nil-account": func(t *testing.T) test { | ||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov) | ||
ctx = context.WithValue(ctx, accContextKey, nil) | ||
|
@@ -636,6 +640,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(), provisionerContextKey, prov) | ||
|
@@ -646,6 +651,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(), provisionerContextKey, prov) | ||
|
@@ -657,6 +663,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(), provisionerContextKey, prov) | ||
|
@@ -665,39 +672,76 @@ 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"} | ||
|
||
"ok/validate-challenge": 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(), provisionerContextKey, prov) | ||
ctx = context.WithValue(ctx, accContextKey, acc) | ||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true}) | ||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) | ||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) | ||
ch := ch() | ||
ch.Status = "valid" | ||
ch.Validated = time.Now().UTC().Format(time.RFC3339) | ||
count := 0 | ||
return test{ | ||
auth: &mockAcmeAuthority{ | ||
err: acme.UnauthorizedErr(nil), | ||
validateChallenge: func(p provisioner.Interface, accID, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) { | ||
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(typ acme.Link, provID string, abs bool, in ...string) string { | ||
var ret string | ||
switch count { | ||
case 0: | ||
assert.Equals(t, typ, acme.AuthzLink) | ||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov)) | ||
assert.True(t, abs) | ||
assert.Equals(t, in, []string{ch.AuthzID}) | ||
ret = fmt.Sprintf("https://ca.smallstep.com/acme/authz/%s", ch.AuthzID) | ||
case 1: | ||
assert.Equals(t, typ, acme.ChallengeLink) | ||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov)) | ||
assert.True(t, abs) | ||
assert.Equals(t, in, []string{ch.ID}) | ||
ret = url | ||
} | ||
count++ | ||
return ret | ||
}, | ||
}, | ||
ctx: ctx, | ||
statusCode: 401, | ||
problem: acme.UnauthorizedErr(nil), | ||
statusCode: 200, | ||
ch: ch, | ||
} | ||
}, | ||
"ok/validate-challenge": func(t *testing.T) test { | ||
|
||
"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(), provisionerContextKey, prov) | ||
ctx = context.WithValue(ctx, accContextKey, acc) | ||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) | ||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) | ||
chiCtxInactive := chi.NewRouteContext() | ||
chiCtxInactive.URLParams.Add("chID", "chID") | ||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtxInactive) | ||
ch := ch() | ||
ch.Status = "valid" | ||
ch.Validated = time.Now().UTC().Format(time.RFC3339) | ||
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, payloadContextKey, &payloadInfo{value: chJSON}) | ||
count := 0 | ||
return test{ | ||
auth: &mockAcmeAuthority{ | ||
|
@@ -728,12 +772,15 @@ func TestHandlerGetChallenge(t *testing.T) { | |
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) { | ||
|
@@ -760,13 +807,21 @@ 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("<https://ca.smallstep.com/acme/authz/%s>;rel=\"up\"", 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": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be misunderstanding the code, but if the status is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think I need to add the case for "invalid" here. Good catch. These tests pass though, so I don't think we're actually testing that path, currently. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But that would happen below. If the challenge is processing up here we return the challenge in the processing state with retryafter information. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I was just saying 1) it would be nice to have a case for invalid, and 2) it would be nice to have an invalid test. |
||
assert.Equals(t, res.Header["Cache-Control"], []string{"no-cache"}) | ||
assert.Equals(t, res.Header["Retry-After"], []string{tc.ch.RetryAfter}) | ||
case "valid", "invalid": | ||
assert.Equals(t, res.Header["Location"], []string{url}) | ||
assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<https://ca.smallstep.com/acme/authz/%s>;rel=\"up\"", tc.ch.AuthzID)}) | ||
} | ||
} else { | ||
assert.Fatal(t, false, "Unexpected Status Code") | ||
} | ||
}) | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.