Skip to content

Commit b01c7a7

Browse files
author
Alex Vaghin
committed
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 <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent ccddf37 commit b01c7a7

File tree

2 files changed

+126
-91
lines changed

2 files changed

+126
-91
lines changed

Diff for: acme/acme.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,20 @@ func (c *Client) UpdateReg(ctx context.Context, a *Account) (*Account, error) {
323323
// a valid authorization (Authorization.Status is StatusValid). If so, the caller
324324
// need not fulfill any challenge and can proceed to requesting a certificate.
325325
func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) {
326+
return c.authorize(ctx, "dns", domain)
327+
}
328+
329+
// AuthorizeIP is the same as Authorize but requests IP address authorization.
330+
// Clients which successfully obtain such authorization may request to issue
331+
// a certificate for IP addresses.
332+
//
333+
// See the ACME spec extension for more details about IP address identifiers:
334+
// https://tools.ietf.org/html/draft-ietf-acme-ip.
335+
func (c *Client) AuthorizeIP(ctx context.Context, ipaddr string) (*Authorization, error) {
336+
return c.authorize(ctx, "ip", ipaddr)
337+
}
338+
339+
func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization, error) {
326340
if _, err := c.Discover(ctx); err != nil {
327341
return nil, err
328342
}
@@ -336,7 +350,7 @@ func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization,
336350
Identifier authzID `json:"identifier"`
337351
}{
338352
Resource: "new-authz",
339-
Identifier: authzID{Type: "dns", Value: domain},
353+
Identifier: authzID{Type: typ, Value: val},
340354
}
341355
res, err := c.post(ctx, c.Key, c.dir.AuthzURL, req, wantStatus(http.StatusCreated))
342356
if err != nil {

Diff for: acme/acme_test.go

+111-90
Original file line numberDiff line numberDiff line change
@@ -288,106 +288,127 @@ func TestGetReg(t *testing.T) {
288288
}
289289

290290
func TestAuthorize(t *testing.T) {
291-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
292-
if r.Method == "HEAD" {
293-
w.Header().Set("Replay-Nonce", "test-nonce")
294-
return
295-
}
296-
if r.Method != "POST" {
297-
t.Errorf("r.Method = %q; want POST", r.Method)
298-
}
291+
tt := []struct{ typ, value string }{
292+
{"dns", "example.com"},
293+
{"ip", "1.2.3.4"},
294+
}
295+
for _, test := range tt {
296+
t.Run(test.typ, func(t *testing.T) {
297+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
298+
if r.Method == "HEAD" {
299+
w.Header().Set("Replay-Nonce", "test-nonce")
300+
return
301+
}
302+
if r.Method != "POST" {
303+
t.Errorf("r.Method = %q; want POST", r.Method)
304+
}
299305

300-
var j struct {
301-
Resource string
302-
Identifier struct {
303-
Type string
304-
Value string
305-
}
306-
}
307-
decodeJWSRequest(t, &j, r)
306+
var j struct {
307+
Resource string
308+
Identifier struct {
309+
Type string
310+
Value string
311+
}
312+
}
313+
decodeJWSRequest(t, &j, r)
308314

309-
// Test request
310-
if j.Resource != "new-authz" {
311-
t.Errorf("j.Resource = %q; want new-authz", j.Resource)
312-
}
313-
if j.Identifier.Type != "dns" {
314-
t.Errorf("j.Identifier.Type = %q; want dns", j.Identifier.Type)
315-
}
316-
if j.Identifier.Value != "example.com" {
317-
t.Errorf("j.Identifier.Value = %q; want example.com", j.Identifier.Value)
318-
}
315+
// Test request
316+
if j.Resource != "new-authz" {
317+
t.Errorf("j.Resource = %q; want new-authz", j.Resource)
318+
}
319+
if j.Identifier.Type != test.typ {
320+
t.Errorf("j.Identifier.Type = %q; want %q", j.Identifier.Type, test.typ)
321+
}
322+
if j.Identifier.Value != test.value {
323+
t.Errorf("j.Identifier.Value = %q; want %q", j.Identifier.Value, test.value)
324+
}
319325

320-
w.Header().Set("Location", "https://ca.tld/acme/auth/1")
321-
w.WriteHeader(http.StatusCreated)
322-
fmt.Fprintf(w, `{
323-
"identifier": {"type":"dns","value":"example.com"},
324-
"status":"pending",
325-
"challenges":[
326-
{
327-
"type":"http-01",
328-
"status":"pending",
329-
"uri":"https://ca.tld/acme/challenge/publickey/id1",
330-
"token":"token1"
331-
},
332-
{
333-
"type":"tls-sni-01",
326+
w.Header().Set("Location", "https://ca.tld/acme/auth/1")
327+
w.WriteHeader(http.StatusCreated)
328+
fmt.Fprintf(w, `{
329+
"identifier": {"type":%q,"value":%q},
334330
"status":"pending",
335-
"uri":"https://ca.tld/acme/challenge/publickey/id2",
336-
"token":"token2"
337-
}
338-
],
339-
"combinations":[[0],[1]]}`)
340-
}))
341-
defer ts.Close()
331+
"challenges":[
332+
{
333+
"type":"http-01",
334+
"status":"pending",
335+
"uri":"https://ca.tld/acme/challenge/publickey/id1",
336+
"token":"token1"
337+
},
338+
{
339+
"type":"tls-sni-01",
340+
"status":"pending",
341+
"uri":"https://ca.tld/acme/challenge/publickey/id2",
342+
"token":"token2"
343+
}
344+
],
345+
"combinations":[[0],[1]]
346+
}`, test.typ, test.value)
347+
}))
348+
defer ts.Close()
349+
350+
var (
351+
auth *Authorization
352+
err error
353+
)
354+
cl := Client{Key: testKeyEC, dir: &Directory{AuthzURL: ts.URL}}
355+
switch test.typ {
356+
case "dns":
357+
auth, err = cl.Authorize(context.Background(), test.value)
358+
case "ip":
359+
auth, err = cl.AuthorizeIP(context.Background(), test.value)
360+
default:
361+
t.Fatalf("unknown identifier type: %q", test.typ)
362+
}
363+
if err != nil {
364+
t.Fatal(err)
365+
}
342366

343-
cl := Client{Key: testKeyEC, dir: &Directory{AuthzURL: ts.URL}}
344-
auth, err := cl.Authorize(context.Background(), "example.com")
345-
if err != nil {
346-
t.Fatal(err)
347-
}
367+
if auth.URI != "https://ca.tld/acme/auth/1" {
368+
t.Errorf("URI = %q; want https://ca.tld/acme/auth/1", auth.URI)
369+
}
370+
if auth.Status != "pending" {
371+
t.Errorf("Status = %q; want pending", auth.Status)
372+
}
373+
if auth.Identifier.Type != test.typ {
374+
t.Errorf("Identifier.Type = %q; want %q", auth.Identifier.Type, test.typ)
375+
}
376+
if auth.Identifier.Value != test.value {
377+
t.Errorf("Identifier.Value = %q; want %q", auth.Identifier.Value, test.value)
378+
}
348379

349-
if auth.URI != "https://ca.tld/acme/auth/1" {
350-
t.Errorf("URI = %q; want https://ca.tld/acme/auth/1", auth.URI)
351-
}
352-
if auth.Status != "pending" {
353-
t.Errorf("Status = %q; want pending", auth.Status)
354-
}
355-
if auth.Identifier.Type != "dns" {
356-
t.Errorf("Identifier.Type = %q; want dns", auth.Identifier.Type)
357-
}
358-
if auth.Identifier.Value != "example.com" {
359-
t.Errorf("Identifier.Value = %q; want example.com", auth.Identifier.Value)
360-
}
380+
if n := len(auth.Challenges); n != 2 {
381+
t.Fatalf("len(auth.Challenges) = %d; want 2", n)
382+
}
361383

362-
if n := len(auth.Challenges); n != 2 {
363-
t.Fatalf("len(auth.Challenges) = %d; want 2", n)
364-
}
384+
c := auth.Challenges[0]
385+
if c.Type != "http-01" {
386+
t.Errorf("c.Type = %q; want http-01", c.Type)
387+
}
388+
if c.URI != "https://ca.tld/acme/challenge/publickey/id1" {
389+
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI)
390+
}
391+
if c.Token != "token1" {
392+
t.Errorf("c.Token = %q; want token1", c.Token)
393+
}
365394

366-
c := auth.Challenges[0]
367-
if c.Type != "http-01" {
368-
t.Errorf("c.Type = %q; want http-01", c.Type)
369-
}
370-
if c.URI != "https://ca.tld/acme/challenge/publickey/id1" {
371-
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id1", c.URI)
372-
}
373-
if c.Token != "token1" {
374-
t.Errorf("c.Token = %q; want token1", c.Token)
375-
}
395+
c = auth.Challenges[1]
396+
if c.Type != "tls-sni-01" {
397+
t.Errorf("c.Type = %q; want tls-sni-01", c.Type)
398+
}
399+
if c.URI != "https://ca.tld/acme/challenge/publickey/id2" {
400+
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id2", c.URI)
401+
}
402+
if c.Token != "token2" {
403+
t.Errorf("c.Token = %q; want token2", c.Token)
404+
}
376405

377-
c = auth.Challenges[1]
378-
if c.Type != "tls-sni-01" {
379-
t.Errorf("c.Type = %q; want tls-sni-01", c.Type)
380-
}
381-
if c.URI != "https://ca.tld/acme/challenge/publickey/id2" {
382-
t.Errorf("c.URI = %q; want https://ca.tld/acme/challenge/publickey/id2", c.URI)
383-
}
384-
if c.Token != "token2" {
385-
t.Errorf("c.Token = %q; want token2", c.Token)
386-
}
406+
combs := [][]int{{0}, {1}}
407+
if !reflect.DeepEqual(auth.Combinations, combs) {
408+
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
409+
}
387410

388-
combs := [][]int{{0}, {1}}
389-
if !reflect.DeepEqual(auth.Combinations, combs) {
390-
t.Errorf("auth.Combinations: %+v\nwant: %+v\n", auth.Combinations, combs)
411+
})
391412
}
392413
}
393414

0 commit comments

Comments
 (0)