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>
diff --git a/acme/acme.go b/acme/acme.go
index c6fd998..edd1c82 100644
--- a/acme/acme.go
+++ b/acme/acme.go
@@ -323,6 +323,20 @@
 // 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 @@
 		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 {
diff --git a/acme/acme_test.go b/acme/acme_test.go
index ef1fe47..29d7abb 100644
--- a/acme/acme_test.go
+++ b/acme/acme_test.go
@@ -288,106 +288,127 @@
 }
 
 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
 				}
-			],
-			"combinations":[[0],[1]]}`)
-	}))
-	defer ts.Close()
+				if r.Method != "POST" {
+					t.Errorf("r.Method = %q; want POST", r.Method)
+				}
 
-	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)
+			}
+
+		})
 	}
 }