mirror of https://github.com/shred/acme4j
Add support for draft-aaron-acme-profiles
parent
318aeaab9d
commit
19371229b8
|
@ -131,6 +131,31 @@ public class Metadata {
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the CA supports the profile feature.
|
||||||
|
*
|
||||||
|
* @since 3.5
|
||||||
|
* @throws AcmeNotSupportedException if the server does not support the profile feature.
|
||||||
|
*/
|
||||||
|
public boolean isProfileAllowed() {
|
||||||
|
return meta.getFeature("profile").optional().isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the CA supports the requested profile.
|
||||||
|
*
|
||||||
|
* @since 3.5
|
||||||
|
* @throws AcmeNotSupportedException if the server does not support the requested profile.
|
||||||
|
*/
|
||||||
|
public boolean isProfileAllowed(String profile) {
|
||||||
|
return meta.getFeature("profile").optional()
|
||||||
|
.map(Value::asObject)
|
||||||
|
.orElseGet(JSON::empty)
|
||||||
|
.get(profile)
|
||||||
|
.optional()
|
||||||
|
.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the CA supports subdomain auth according to RFC9444.
|
* Returns whether the CA supports subdomain auth according to RFC9444.
|
||||||
*
|
*
|
||||||
|
|
|
@ -54,6 +54,7 @@ public class OrderBuilder {
|
||||||
private @Nullable Duration autoRenewalLifetime;
|
private @Nullable Duration autoRenewalLifetime;
|
||||||
private @Nullable Duration autoRenewalLifetimeAdjust;
|
private @Nullable Duration autoRenewalLifetimeAdjust;
|
||||||
private boolean autoRenewalGet;
|
private boolean autoRenewalGet;
|
||||||
|
private @Nullable String profile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link OrderBuilder}.
|
* Create a new {@link OrderBuilder}.
|
||||||
|
@ -269,6 +270,25 @@ public class OrderBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the CA of the desired profile of the ordered certificate.
|
||||||
|
* <p>
|
||||||
|
* Optional, only supported if the CA supports profiles. However, in this
|
||||||
|
* case the client <em>may</em> include this field.
|
||||||
|
*
|
||||||
|
* @param profile
|
||||||
|
* Identifier of the desired profile
|
||||||
|
* @return itself
|
||||||
|
* @draft This method is currently based on RFC draft draft-aaron-acme-profiles. It may be changed or removed
|
||||||
|
* without notice to reflect future changes to the draft. SemVer rules do not apply
|
||||||
|
* here.
|
||||||
|
* @since 3.5.0
|
||||||
|
*/
|
||||||
|
public OrderBuilder profile(String profile) {
|
||||||
|
this.profile = Objects.requireNonNull(profile);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the CA that the ordered certificate will replace a previously issued
|
* Notifies the CA that the ordered certificate will replace a previously issued
|
||||||
* certificate. The certificate is identified by its ARI unique identifier.
|
* certificate. The certificate is identified by its ARI unique identifier.
|
||||||
|
@ -351,6 +371,14 @@ public class OrderBuilder {
|
||||||
throw new AcmeNotSupportedException("renewal-information");
|
throw new AcmeNotSupportedException("renewal-information");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile != null && !session.getMetadata().isProfileAllowed()) {
|
||||||
|
throw new AcmeNotSupportedException("profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile != null && !session.getMetadata().isProfileAllowed(profile)) {
|
||||||
|
throw new AcmeNotSupportedException("profile with value " + profile);
|
||||||
|
}
|
||||||
|
|
||||||
var hasAncestorDomain = identifierSet.stream()
|
var hasAncestorDomain = identifierSet.stream()
|
||||||
.filter(id -> Identifier.TYPE_DNS.equals(id.getType()))
|
.filter(id -> Identifier.TYPE_DNS.equals(id.getType()))
|
||||||
.anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN));
|
.anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN));
|
||||||
|
@ -393,6 +421,10 @@ public class OrderBuilder {
|
||||||
claims.put("replaces", replaces);
|
claims.put("replaces", replaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(profile != null) {
|
||||||
|
claims.put("profile", profile);
|
||||||
|
}
|
||||||
|
|
||||||
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
|
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
|
||||||
|
|
||||||
var order = new Order(login, conn.getLocation());
|
var order = new Order(login, conn.getLocation());
|
||||||
|
|
|
@ -335,6 +335,111 @@ public class OrderBuilderTest {
|
||||||
provider.close();
|
provider.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a new profile {@link Order} can be created.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testProfileOrderCertificate() throws Exception {
|
||||||
|
|
||||||
|
var provider = new TestableConnectionProvider() {
|
||||||
|
@Override
|
||||||
|
public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
|
||||||
|
assertThat(url).isEqualTo(resourceUrl);
|
||||||
|
assertThatJson(claims.toString()).isEqualTo(getJSON("requestProfileOrderRequest").toString());
|
||||||
|
assertThat(login).isNotNull();
|
||||||
|
return HttpURLConnection.HTTP_CREATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSON readJsonResponse() {
|
||||||
|
return getJSON("requestAutoRenewOrderResponse");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getLocation() {
|
||||||
|
return locationUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var login = provider.createLogin();
|
||||||
|
|
||||||
|
provider.putMetadata("profile",JSON.parse(
|
||||||
|
"{\"classic\": true}"
|
||||||
|
).toMap());
|
||||||
|
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||||
|
|
||||||
|
var account = new Account(login);
|
||||||
|
account.newOrder()
|
||||||
|
.domain("example.org")
|
||||||
|
.profile("classic")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
provider.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a profile {@link Order} cannot be created if the profile is unsupported by the CA.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testUnsupportedProfileOrderCertificateFails() throws Exception {
|
||||||
|
|
||||||
|
var provider = new TestableConnectionProvider() {
|
||||||
|
@Override
|
||||||
|
public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
|
||||||
|
assertThat(url).isEqualTo(resourceUrl);
|
||||||
|
assertThatJson(claims.toString()).isEqualTo(getJSON("requestProfileOrderRequest").toString());
|
||||||
|
assertThat(login).isNotNull();
|
||||||
|
return HttpURLConnection.HTTP_CREATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSON readJsonResponse() {
|
||||||
|
return getJSON("requestAutoRenewOrderResponse");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getLocation() {
|
||||||
|
return locationUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertThrows(AcmeNotSupportedException.class, () -> {
|
||||||
|
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||||
|
|
||||||
|
var login = provider.createLogin();
|
||||||
|
|
||||||
|
var account = new Account(login);
|
||||||
|
account.newOrder()
|
||||||
|
.domain("example.org")
|
||||||
|
.profile("invalid")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
provider.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a profile {@link Order} cannot be created if the feature is unsupported by the CA.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testProfileOrderCertificateFails() {
|
||||||
|
assertThrows(AcmeNotSupportedException.class, () -> {
|
||||||
|
var provider = new TestableConnectionProvider();
|
||||||
|
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||||
|
|
||||||
|
var login = provider.createLogin();
|
||||||
|
|
||||||
|
var account = new Account(login);
|
||||||
|
account.newOrder()
|
||||||
|
.domain("example.org")
|
||||||
|
.profile("classic")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
provider.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that the ARI replaces field is set.
|
* Test that the ARI replaces field is set.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -183,6 +183,9 @@ public class SessionTest {
|
||||||
softly.assertThat(meta.getAutoRenewalMaxDuration()).isEqualTo(Duration.ofDays(365));
|
softly.assertThat(meta.getAutoRenewalMaxDuration()).isEqualTo(Duration.ofDays(365));
|
||||||
softly.assertThat(meta.getAutoRenewalMinLifetime()).isEqualTo(Duration.ofHours(24));
|
softly.assertThat(meta.getAutoRenewalMinLifetime()).isEqualTo(Duration.ofHours(24));
|
||||||
softly.assertThat(meta.isAutoRenewalGetAllowed()).isTrue();
|
softly.assertThat(meta.isAutoRenewalGetAllowed()).isTrue();
|
||||||
|
softly.assertThat(meta.isProfileAllowed()).isTrue();
|
||||||
|
softly.assertThat(meta.isProfileAllowed("classic")).isTrue();
|
||||||
|
softly.assertThat(meta.isProfileAllowed("invalid")).isFalse();
|
||||||
softly.assertThat(meta.isExternalAccountRequired()).isTrue();
|
softly.assertThat(meta.isExternalAccountRequired()).isTrue();
|
||||||
softly.assertThat(meta.isSubdomainAuthAllowed()).isTrue();
|
softly.assertThat(meta.isSubdomainAuthAllowed()).isTrue();
|
||||||
softly.assertThat(meta.getJSON()).isNotNull();
|
softly.assertThat(meta.getJSON()).isNotNull();
|
||||||
|
@ -235,6 +238,8 @@ public class SessionTest {
|
||||||
.isThrownBy(meta::getAutoRenewalMinLifetime);
|
.isThrownBy(meta::getAutoRenewalMinLifetime);
|
||||||
softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
|
softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
|
||||||
.isThrownBy(meta::isAutoRenewalGetAllowed);
|
.isThrownBy(meta::isAutoRenewalGetAllowed);
|
||||||
|
softly.assertThat(meta.isProfileAllowed()).isFalse();
|
||||||
|
softly.assertThat(meta.isProfileAllowed("classic")).isFalse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@
|
||||||
"foo",
|
"foo",
|
||||||
"bar",
|
"bar",
|
||||||
"barfoo"
|
"barfoo"
|
||||||
]
|
],
|
||||||
|
"profiles": {
|
||||||
|
"classic": "The profile you're accustomed to",
|
||||||
|
"custom": "Some other profile"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue