acme: support IP address authorization type
This change extends the Client to allow users request certificate issuance for IP addresses. See the ACME spec extension for details about IP address identifiers: https://tools.ietf.org/html/draft-ietf-acme-ip. Change-Id: I92a8d8fae048487168906c14892c6dd33af10c07 Reviewed-on: https://go-review.googlesource.com/c/160197 Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
+15
-1
@@ -323,6 +323,20 @@ func (c *Client) UpdateReg(ctx context.Context, a *Account) (*Account, error) {
|
||||
// a valid authorization (Authorization.Status is StatusValid). If so, the caller
|
||||
// need not fulfill any challenge and can proceed to requesting a certificate.
|
||||
func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) {
|
||||
return c.authorize(ctx, "dns", domain)
|
||||
}
|
||||
|
||||
// AuthorizeIP is the same as Authorize but requests IP address authorization.
|
||||
// Clients which successfully obtain such authorization may request to issue
|
||||
// a certificate for IP addresses.
|
||||
//
|
||||
// See the ACME spec extension for more details about IP address identifiers:
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-ip.
|
||||
func (c *Client) AuthorizeIP(ctx context.Context, ipaddr string) (*Authorization, error) {
|
||||
return c.authorize(ctx, "ip", ipaddr)
|
||||
}
|
||||
|
||||
func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization, error) {
|
||||
if _, err := c.Discover(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -336,7 +350,7 @@ func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization,
|
||||
Identifier authzID `json:"identifier"`
|
||||
}{
|
||||
Resource: "new-authz",
|
||||
Identifier: authzID{Type: "dns", Value: domain},
|
||||
Identifier: authzID{Type: typ, Value: val},
|
||||
}
|
||||
res, err := c.post(ctx, c.Key, c.dir.AuthzURL, req, wantStatus(http.StatusCreated))
|
||||
if err != nil {
|
||||
|
||||
+114
-93
@@ -288,106 +288,127 @@ func TestGetReg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "HEAD" {
|
||||
w.Header().Set("Replay-Nonce", "test-nonce")
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("r.Method = %q; want POST", r.Method)
|
||||
}
|
||||
|
||||
var j struct {
|
||||
Resource string
|
||||
Identifier struct {
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
}
|
||||
decodeJWSRequest(t, &j, r)
|
||||
|
||||
// Test request
|
||||
if j.Resource != "new-authz" {
|
||||
t.Errorf("j.Resource = %q; want new-authz", j.Resource)
|
||||
}
|
||||
if j.Identifier.Type != "dns" {
|
||||
t.Errorf("j.Identifier.Type = %q; want dns", j.Identifier.Type)
|
||||
}
|
||||
if j.Identifier.Value != "example.com" {
|
||||
t.Errorf("j.Identifier.Value = %q; want example.com", j.Identifier.Value)
|
||||
}
|
||||
|
||||
w.Header().Set("Location", "https://ca.tld/acme/auth/1")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprintf(w, `{
|
||||
"identifier": {"type":"dns","value":"example.com"},
|
||||
"status":"pending",
|
||||
"challenges":[
|
||||
{
|
||||
"type":"http-01",
|
||||
"status":"pending",
|
||||
"uri":"https://ca.tld/acme/challenge/publickey/id1",
|
||||
"token":"token1"
|
||||
},
|
||||
{
|
||||
"type":"tls-sni-01",
|
||||
"status":"pending",
|
||||
"uri":"https://ca.tld/acme/challenge/publickey/id2",
|
||||
"token":"token2"
|
||||
tt := []struct{ typ, value string }{
|
||||
{"dns", "example.com"},
|
||||
{"ip", "1.2.3.4"},
|
||||
}
|
||||
for _, test := range tt {
|
||||
t.Run(test.typ, func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "HEAD" {
|
||||
w.Header().Set("Replay-Nonce", "test-nonce")
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("r.Method = %q; want POST", r.Method)
|
||||
}
|
||||
],
|
||||
"combinations":[[0],[1]]}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cl := Client{Key: testKeyEC, dir: &Directory{AuthzURL: ts.URL}}
|
||||
auth, err := cl.Authorize(context.Background(), "example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var j struct {
|
||||
Resource string
|
||||
Identifier struct {
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
}
|
||||
decodeJWSRequest(t, &j, r)
|
||||
|
||||
if auth.URI != "https://ca.tld/acme/auth/1" {
|
||||
t.Errorf("URI = %q; want https://ca.tld/acme/auth/1", auth.URI)
|
||||
}
|
||||
if auth.Status != "pending" {
|
||||
t.Errorf("Status = %q; want pending", auth.Status)
|
||||
}
|
||||
if auth.Identifier.Type != "dns" {
|
||||
t.Errorf("Identifier.Type = %q; want dns", auth.Identifier.Type)
|
||||
}
|
||||
if auth.Identifier.Value != "example.com" {
|
||||
t.Errorf("Identifier.Value = %q; want example.com", auth.Identifier.Value)
|
||||
}
|
||||
// Test request
|
||||
if j.Resource != "new-authz" {
|
||||
t.Errorf("j.Resource = %q; want new-authz", j.Resource)
|
||||
}
|
||||
if j.Identifier.Type != test.typ {
|
||||
t.Errorf("j.Identifier.Type = %q; want %q", j.Identifier.Type, test.typ)
|
||||
}
|
||||
if j.Identifier.Value != test.value {
|
||||
t.Errorf("j.Identifier.Value = %q; want %q", j.Identifier.Value, test.value)
|
||||
}
|
||||
|
||||
if n := len(auth.Challenges); n != 2 {
|
||||
t.Fatalf("len(auth.Challenges) = %d; want 2", n)
|
||||
}
|
||||
w.Header().Set("Location", "https://ca.tld/acme/auth/1")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprintf(w, `{
|
||||
"identifier": {"type":%q,"value":%q},
|
||||
"status":"pending",
|
||||
"challenges":[
|
||||
{
|
||||
"type":"http-01",
|
||||
"status":"pending",
|
||||
"uri":"https://ca.tld/acme/challenge/publickey/id1",
|
||||
"token":"token1"
|
||||
},
|
||||
{
|
||||
"type":"tls-sni-01",
|
||||
"status":"pending",
|
||||
"uri":"https://ca.tld/acme/challenge/publickey/id2",
|
||||
"token":"token2"
|
||||
}
|
||||
],
|
||||
"combinations":[[0],[1]]
|
||||
}`, test.typ, test.value)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := auth.Challenges[0]
|
||||
if c.Type != "http-01" {
|
||||
t.Errorf("c.Type = %q; want http-01", c.Type)
|
||||
}
|
||||
if c.URI != "https://ca.tld/acme/challenge/publickey/id1" {
|
||||
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI)
|
||||
}
|
||||
if c.Token != "token1" {
|
||||
t.Errorf("c.Token = %q; want token1", c.Token)
|
||||
}
|
||||
var (
|
||||
auth *Authorization
|
||||
err error
|
||||
)
|
||||
cl := Client{Key: testKeyEC, dir: &Directory{AuthzURL: ts.URL}}
|
||||
switch test.typ {
|
||||
case "dns":
|
||||
auth, err = cl.Authorize(context.Background(), test.value)
|
||||
case "ip":
|
||||
auth, err = cl.AuthorizeIP(context.Background(), test.value)
|
||||
default:
|
||||
t.Fatalf("unknown identifier type: %q", test.typ)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c = auth.Challenges[1]
|
||||
if c.Type != "tls-sni-01" {
|
||||
t.Errorf("c.Type = %q; want tls-sni-01", c.Type)
|
||||
}
|
||||
if c.URI != "https://ca.tld/acme/challenge/publickey/id2" {
|
||||
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id2", c.URI)
|
||||
}
|
||||
if c.Token != "token2" {
|
||||
t.Errorf("c.Token = %q; want token2", c.Token)
|
||||
}
|
||||
if auth.URI != "https://ca.tld/acme/auth/1" {
|
||||
t.Errorf("URI = %q; want https://ca.tld/acme/auth/1", auth.URI)
|
||||
}
|
||||
if auth.Status != "pending" {
|
||||
t.Errorf("Status = %q; want pending", auth.Status)
|
||||
}
|
||||
if auth.Identifier.Type != test.typ {
|
||||
t.Errorf("Identifier.Type = %q; want %q", auth.Identifier.Type, test.typ)
|
||||
}
|
||||
if auth.Identifier.Value != test.value {
|
||||
t.Errorf("Identifier.Value = %q; want %q", auth.Identifier.Value, test.value)
|
||||
}
|
||||
|
||||
combs := [][]int{{0}, {1}}
|
||||
if !reflect.DeepEqual(auth.Combinations, combs) {
|
||||
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
|
||||
if n := len(auth.Challenges); n != 2 {
|
||||
t.Fatalf("len(auth.Challenges) = %d; want 2", n)
|
||||
}
|
||||
|
||||
c := auth.Challenges[0]
|
||||
if c.Type != "http-01" {
|
||||
t.Errorf("c.Type = %q; want http-01", c.Type)
|
||||
}
|
||||
if c.URI != "https://ca.tld/acme/challenge/publickey/id1" {
|
||||
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI)
|
||||
}
|
||||
if c.Token != "token1" {
|
||||
t.Errorf("c.Token = %q; want token1", c.Token)
|
||||
}
|
||||
|
||||
c = auth.Challenges[1]
|
||||
if c.Type != "tls-sni-01" {
|
||||
t.Errorf("c.Type = %q; want tls-sni-01", c.Type)
|
||||
}
|
||||
if c.URI != "https://ca.tld/acme/challenge/publickey/id2" {
|
||||
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id2", c.URI)
|
||||
}
|
||||
if c.Token != "token2" {
|
||||
t.Errorf("c.Token = %q; want token2", c.Token)
|
||||
}
|
||||
|
||||
combs := [][]int{{0}, {1}}
|
||||
if !reflect.DeepEqual(auth.Combinations, combs) {
|
||||
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user