Support more identifier types

pull/81/head
Richard Körber 2018-08-20 23:07:07 +02:00
parent 57b050c868
commit 3689ab5e5e
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
15 changed files with 324 additions and 62 deletions

View File

@ -14,7 +14,7 @@
package org.shredzone.acme4j;
import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.*;
import static org.shredzone.acme4j.toolbox.AcmeUtils.keyAlgorithm;
import java.net.URI;
import java.net.URL;
@ -160,9 +160,7 @@ public class Account extends AcmeJsonResource {
LOG.debug("preAuthorizeDomain {}", domain);
try (Connection conn = connect()) {
JSONBuilder claims = new JSONBuilder();
claims.object("identifier")
.put("type", "dns")
.put("value", toAce(domain));
claims.put("identifier", Identifier.dns(domain).toMap());
conn.sendSignedRequest(newAuthzUrl, claims, getLogin());

View File

@ -52,14 +52,25 @@ public class Authorization extends AcmeJsonResource {
* For wildcard domain orders, the domain itself (without wildcard prefix) is returned
* here. To find out if this {@link Authorization} is related to a wildcard domain
* order, check the {@link #isWildcard()} method.
*
* @deprecated Use {@link #getIdentifier()}.
*/
@Deprecated
public String getDomain() {
JSON jsonIdentifier = getJSON().get("identifier").asObject();
String type = jsonIdentifier.get("type").asString();
if (!"dns".equals(type)) {
throw new AcmeProtocolException("Unknown authorization type: " + type);
}
return jsonIdentifier.get("value").asString();
return getIdentifier().getDomain();
}
/**
* Gets the {@link Identifier} to be authorized.
* <p>
* For wildcard domain orders, the domain itself (without wildcard prefix) is returned
* here. To find out if this {@link Authorization} is related to a wildcard domain
* order, check the {@link #isWildcard()} method.
*
* @since 2.3
*/
public Identifier getIdentifier() {
return getJSON().get("identifier").asIdentifier();
}
/**

View File

@ -0,0 +1,149 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2018 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j;
import static java.util.Objects.requireNonNull;
import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
import java.io.Serializable;
import java.util.Map;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.concurrent.Immutable;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
/**
* Represents an identifier.
* <p>
* The ACME protocol only defines the DNS identifier, which identifies a domain name.
* <p>
* CAs may define further, proprietary identifier types.
*
* @since 2.3
*/
@ParametersAreNonnullByDefault
@Immutable
public class Identifier implements Serializable {
private static final long serialVersionUID = -7777851842076362412L;
/**
* Type constant for DNS identifiers.
*/
public static final String DNS = "dns";
private static final String KEY_TYPE = "type";
private static final String KEY_VALUE = "value";
private final String type;
private final String value;
/**
* Creates a new DNS identifier for the given domain name.
*
* @param domain
* Domain name. Unicode domains are automatically ASCII encoded.
* @return New {@link Identifier}
*/
public static Identifier dns(String domain) {
return new Identifier(DNS, toAce(domain));
}
/**
* Creates a new {@link Identifier}.
* <p>
* This is a generic constructor for identifiers. Refer to the documentation of your
* CA to find out about the accepted identifier types and values.
* <p>
* Note that for DNS identifiers, no ASCII encoding of unicode domain takes place
* here. Use {@link #dns(String)} instead.
*
* @param type
* Identifier type
* @param value
* Identifier value
*/
public Identifier(String type, String value) {
this.type = requireNonNull(type, KEY_TYPE);
this.value = requireNonNull(value, KEY_VALUE);
}
/**
* Creates a new {@link Identifier} from the given {@link JSON} structure.
*
* @param json
* {@link JSON} containing the identifier data
*/
public Identifier(JSON json) {
this(json.get(KEY_TYPE).asString(), json.get(KEY_VALUE).asString());
}
/**
* Returns the identifier type.
*/
public String getType() {
return type;
}
/**
* Returns the identifier value.
*/
public String getValue() {
return value;
}
/**
* Returns the domain name if this is a DNS identifier.
*
* @return Domain name. Unicode domains are ASCII encoded.
* @throws AcmeProtocolException
* if this is not a DNS identifier.
*/
public String getDomain() {
if (!DNS.equals(type)) {
throw new AcmeProtocolException("expected 'dns' identifier, but found '" + type + "'");
}
return value;
}
/**
* Returns the identifier as JSON map.
*/
public Map<String, Object> toMap() {
return new JSONBuilder().put(KEY_TYPE, type).put(KEY_VALUE, value).toMap();
}
@Override
public String toString() {
return type + "=" + value;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Identifier)) {
return false;
}
Identifier i = (Identifier) obj;
return type.equals(i.type) && value.equals(i.value);
}
@Override
public int hashCode() {
return type.hashCode() ^ value.hashCode();
};
}

View File

@ -14,12 +14,11 @@
package org.shredzone.acme4j;
import static java.util.Objects.requireNonNull;
import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
import static java.util.stream.Collectors.toList;
import java.net.URL;
import java.time.Instant;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
@ -43,7 +42,7 @@ public class OrderBuilder {
private final Login login;
private final Set<String> domainSet = new LinkedHashSet<>();
private final Set<Identifier> identifierSet = new LinkedHashSet<>();
private Instant notBefore;
private Instant notAfter;
@ -66,7 +65,7 @@ public class OrderBuilder {
* @return itself
*/
public OrderBuilder domain(String domain) {
domainSet.add(toAce(requireNonNull(domain, "domain")));
identifierSet.add(Identifier.dns(domain));
return this;
}
@ -128,25 +127,16 @@ public class OrderBuilder {
* @return {@link Order} that was created
*/
public Order create() throws AcmeException {
if (domainSet.isEmpty()) {
throw new IllegalArgumentException("At least one domain is required");
if (identifierSet.isEmpty()) {
throw new IllegalArgumentException("At least one identifer is required");
}
Session session = login.getSession();
Object[] identifiers = new Object[domainSet.size()];
Iterator<String> di = domainSet.iterator();
for (int ix = 0; ix < identifiers.length; ix++) {
identifiers[ix] = new JSONBuilder()
.put("type", "dns")
.put("value", di.next())
.toMap();
}
LOG.debug("create");
try (Connection conn = session.provider().connect()) {
JSONBuilder claims = new JSONBuilder();
claims.array("identifiers", identifiers);
claims.array("identifiers", identifierSet.stream().map(Identifier::toMap).collect(toList()));
if (notBefore != null) {
claims.put("notBefore", notBefore);

View File

@ -114,22 +114,27 @@ public class Problem implements Serializable {
/**
* Returns the domain this problem relates to. May be {@code null}.
*
* @deprecated Use {@link #getIdentifier()}.
*/
@Deprecated
@CheckForNull
public String getDomain() {
Value identifier = problemJson.get("identifier");
if (!identifier.isPresent()) {
return null;
}
Identifier identifier = getIdentifier();
return identifier != null ? identifier.getDomain() : null;
}
JSON json = identifier.asObject();
String type = json.get("type").asString();
if (!"dns".equals(type)) {
throw new AcmeProtocolException("Cannot process a " + type + " identifier");
}
return json.get("value").asString();
/**
* Returns the {@link Identifier} this problem relates to. May be {@code null}.
*
* @since 2.3
*/
@CheckForNull
public Identifier getIdentifier() {
return problemJson.get("identifier")
.optional()
.map(Value::asIdentifier)
.orElse(null);
}
/**

View File

@ -25,6 +25,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -166,14 +167,10 @@ public final class AcmeUtils {
*
* @param domain
* Domain name to encode
* @return Encoded domain name, white space trimmed and lower cased. {@code null} if
* {@code null} was passed in.
* @return Encoded domain name, white space trimmed and lower cased.
*/
@CheckForNull
public static String toAce(@Nullable String domain) {
if (domain == null) {
return null;
}
public static String toAce(String domain) {
Objects.requireNonNull(domain, "domain");
return IDN.toASCII(domain.trim()).toLowerCase();
}

View File

@ -48,6 +48,7 @@ import javax.annotation.concurrent.Immutable;
import org.jose4j.json.JsonUtil;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Identifier;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.exception.AcmeProtocolException;
@ -349,6 +350,16 @@ public final class JSON implements Serializable {
return new Problem(asObject(), baseUrl);
}
/**
* Returns the value as {@link Identifier}.
*
* @since 2.3
*/
public Identifier asIdentifier() {
required();
return new Identifier(asObject());
}
/**
* Returns the value as {@link JSON.Array}.
* <p>

View File

@ -19,6 +19,7 @@ import java.security.Key;
import java.security.PublicKey;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@ -137,10 +138,10 @@ public class JSONBuilder {
* @param key
* Property key
* @param values
* Array of property values
* Collection of property values
* @return {@code this}
*/
public JSONBuilder array(String key, Object... values) {
public JSONBuilder array(String key, Collection<?> values) {
data.put(key, values);
return this;
}

View File

@ -75,7 +75,7 @@ public class AccountTest {
public void sendRequest(URL url, Session session) {
if (url("https://example.com/acme/acct/1/orders").equals(url)) {
jsonResponse = new JSONBuilder()
.array("orders", "https://example.com/acme/order/1")
.array("orders", Arrays.asList("https://example.com/acme/order/1"))
.toJSON();
}
}
@ -169,6 +169,7 @@ public class AccountTest {
* Test that a domain can be pre-authorized.
*/
@Test
@SuppressWarnings("deprecation")
public void testPreAuthorizeDomain() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
@ -202,6 +203,7 @@ public class AccountTest {
Authorization auth = account.preAuthorizeDomain(domainName);
assertThat(auth.getDomain(), is(domainName));
assertThat(auth.getIdentifier().getDomain(), is(domainName));
assertThat(auth.getStatus(), is(Status.PENDING));
assertThat(auth.getExpires(), is(nullValue()));
assertThat(auth.getLocation(), is(locationUrl));

View File

@ -88,6 +88,7 @@ public class AuthorizationTest {
* Test that authorization is properly updated.
*/
@Test
@SuppressWarnings("deprecation")
public void testUpdate() throws Exception {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
@ -116,6 +117,7 @@ public class AuthorizationTest {
auth.update();
assertThat(auth.getDomain(), is("example.org"));
assertThat(auth.getIdentifier().getDomain(), is("example.org"));
assertThat(auth.getStatus(), is(Status.VALID));
assertThat(auth.isWildcard(), is(false));
assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z")));
@ -158,7 +160,7 @@ public class AuthorizationTest {
Authorization auth = new Authorization(login, locationUrl);
auth.update();
assertThat(auth.getDomain(), is("example.org"));
assertThat(auth.getIdentifier().getDomain(), is("example.org"));
assertThat(auth.getStatus(), is(Status.VALID));
assertThat(auth.isWildcard(), is(true));
assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z")));
@ -205,12 +207,12 @@ public class AuthorizationTest {
// Lazy loading
assertThat(requestWasSent.get(), is(false));
assertThat(auth.getDomain(), is("example.org"));
assertThat(auth.getIdentifier().getDomain(), is("example.org"));
assertThat(requestWasSent.get(), is(true));
// Subsequent queries do not trigger another load
requestWasSent.set(false);
assertThat(auth.getDomain(), is("example.org"));
assertThat(auth.getIdentifier().getDomain(), is("example.org"));
assertThat(auth.getStatus(), is(Status.VALID));
assertThat(auth.isWildcard(), is(false));
assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z")));
@ -258,7 +260,7 @@ public class AuthorizationTest {
assertThat(ex.getRetryAfter(), is(retryAfter));
}
assertThat(auth.getDomain(), is("example.org"));
assertThat(auth.getIdentifier().getDomain(), is("example.org"));
assertThat(auth.getStatus(), is(Status.VALID));
assertThat(auth.isWildcard(), is(false));
assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z")));

View File

@ -0,0 +1,97 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2018 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.Map;
import org.junit.Test;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.JSONBuilder;
/**
* Unit tests for {@link Identifier}.
*/
public class IdentifierTest {
@Test
public void testConstants() {
assertThat(Identifier.DNS, is("dns"));
}
@Test
public void testGetters() {
Identifier id1 = new Identifier("foo", "123.456");
assertThat(id1.getType(), is("foo"));
assertThat(id1.getValue(), is("123.456"));
assertThat(id1.toString(), is("foo=123.456"));
Map<String, Object> map1 = id1.toMap();
assertThat(map1.size(), is(2));
assertThat(map1.get("type"), is("foo"));
assertThat(map1.get("value"), is("123.456"));
JSONBuilder jb = new JSONBuilder();
jb.put("type", "bar");
jb.put("value", "654.321");
Identifier id2 = new Identifier(jb.toJSON());
assertThat(id2.getType(), is("bar"));
assertThat(id2.getValue(), is("654.321"));
assertThat(id2.toString(), is("bar=654.321"));
Map<String, Object> map2 = id2.toMap();
assertThat(map2.size(), is(2));
assertThat(map2.get("type"), is("bar"));
assertThat(map2.get("value"), is("654.321"));
}
@Test
public void testDns() {
Identifier id1 = Identifier.dns("example.com");
assertThat(id1.getType(), is(Identifier.DNS));
assertThat(id1.getValue(), is("example.com"));
assertThat(id1.getDomain(), is("example.com"));
Identifier id2 = Identifier.dns("ëxämþlë.com");
assertThat(id2.getType(), is(Identifier.DNS));
assertThat(id2.getValue(), is("xn--xml-qla7ae5k.com"));
assertThat(id2.getDomain(), is("xn--xml-qla7ae5k.com"));
}
@Test(expected = AcmeProtocolException.class)
public void testNoDns() {
new Identifier("foo", "example.com").getDomain();
}
@Test
public void testEquals() {
Identifier idRef = new Identifier("foo", "123.456");
Identifier id1 = new Identifier("foo", "123.456");
assertThat(idRef.equals(id1), is(true));
Identifier id2 = new Identifier("bar", "654.321");
assertThat(idRef.equals(id2), is(false));
Identifier id3 = new Identifier("foo", "555.666");
assertThat(idRef.equals(id3), is(false));
Identifier id4 = new Identifier("sna", "123.456");
assertThat(idRef.equals(id4), is(false));
assertThat(idRef.equals(new Object()), is(false));
assertThat(idRef.equals(null), is(false));
}
}

View File

@ -44,7 +44,7 @@ public class ProblemTest {
assertThat(problem.getTitle(), is("Some of the identifiers requested were rejected"));
assertThat(problem.getDetail(), is("Identifier \"abc12_\" is malformed"));
assertThat(problem.getInstance(), is(URI.create("https://example.com/documents/error.html")));
assertThat(problem.getDomain(), is(nullValue()));
assertThat(problem.getIdentifier(), is(nullValue()));
assertThat(problem.asJSON().toString(), is(sameJSONAs(original.toString())));
assertThat(problem.toString(), is(
"Identifier \"abc12_\" is malformed ("
@ -59,14 +59,14 @@ public class ProblemTest {
assertThat(p1.getType(), is(URI.create("urn:ietf:params:acme:error:malformed")));
assertThat(p1.getTitle(), is(nullValue()));
assertThat(p1.getDetail(), is("Invalid underscore in DNS name \"_example.com\""));
assertThat(p1.getDomain(), is("_example.com"));
assertThat(p1.getIdentifier().getDomain(), is("_example.com"));
assertThat(p1.toString(), is("Invalid underscore in DNS name \"_example.com\""));
Problem p2 = subs.get(1);
assertThat(p2.getType(), is(URI.create("urn:ietf:params:acme:error:rejectedIdentifier")));
assertThat(p2.getTitle(), is(nullValue()));
assertThat(p2.getDetail(), is("This CA will not issue for \"example.net\""));
assertThat(p2.getDomain(), is("example.net"));
assertThat(p2.getIdentifier().getDomain(), is("example.net"));
assertThat(p2.toString(), is("This CA will not issue for \"example.net\""));
}

View File

@ -145,7 +145,7 @@ public class ResourceIteratorTest {
int end = (ix + 1) * RESOURCES_PER_PAGE;
JSONBuilder cb = new JSONBuilder();
cb.array(TYPE, resourceURLs.subList(start, end).toArray());
cb.array(TYPE, resourceURLs.subList(start, end));
return JSON.parse(cb.toString());
}

View File

@ -115,9 +115,6 @@ public class AcmeUtilsTest {
// Test ACE encoded domains, they must not change
assertThat(toAce("xn--exmle-hra7p.xn--m-7ba6w"),
is("xn--exmle-hra7p.xn--m-7ba6w"));
// Test null
assertThat(toAce(null), is(nullValue()));
}
/**

View File

@ -21,6 +21,8 @@ import java.security.KeyPair;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import org.jose4j.json.JsonUtil;
@ -152,17 +154,17 @@ public class JSONBuilderTest {
JSONBuilder res;
JSONBuilder cb1 = new JSONBuilder();
res = cb1.array("ar", new Object[0]);
res = cb1.array("ar", Collections.emptyList());
assertThat(res, is(sameInstance(cb1)));
assertThat(cb1.toString(), is("{\"ar\":[]}"));
JSONBuilder cb2 = new JSONBuilder();
res = cb2.array("ar", 123);
res = cb2.array("ar", Arrays.asList(123));
assertThat(res, is(sameInstance(cb2)));
assertThat(cb2.toString(), is("{\"ar\":[123]}"));
JSONBuilder cb3 = new JSONBuilder();
res = cb3.array("ar", 123, "foo", 456);
res = cb3.array("ar", Arrays.asList(123, "foo", 456));
assertThat(res, is(sameInstance(cb3)));
assertThat(cb3.toString(), is("{\"ar\":[123,\"foo\",456]}"));
}