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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -54,6 +54,7 @@ public class OrderBuilder {
|
|||
private @Nullable Duration autoRenewalLifetime;
|
||||
private @Nullable Duration autoRenewalLifetimeAdjust;
|
||||
private boolean autoRenewalGet;
|
||||
private @Nullable String profile;
|
||||
|
||||
/**
|
||||
* Create a new {@link OrderBuilder}.
|
||||
|
@ -269,6 +270,25 @@ public class OrderBuilder {
|
|||
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
|
||||
* certificate. The certificate is identified by its ARI unique identifier.
|
||||
|
@ -351,6 +371,14 @@ public class OrderBuilder {
|
|||
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()
|
||||
.filter(id -> Identifier.TYPE_DNS.equals(id.getType()))
|
||||
.anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN));
|
||||
|
@ -393,6 +421,10 @@ public class OrderBuilder {
|
|||
claims.put("replaces", replaces);
|
||||
}
|
||||
|
||||
if(profile != null) {
|
||||
claims.put("profile", profile);
|
||||
}
|
||||
|
||||
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
|
||||
|
||||
var order = new Order(login, conn.getLocation());
|
||||
|
|
|
@ -335,6 +335,111 @@ public class OrderBuilderTest {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -183,6 +183,9 @@ public class SessionTest {
|
|||
softly.assertThat(meta.getAutoRenewalMaxDuration()).isEqualTo(Duration.ofDays(365));
|
||||
softly.assertThat(meta.getAutoRenewalMinLifetime()).isEqualTo(Duration.ofHours(24));
|
||||
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.isSubdomainAuthAllowed()).isTrue();
|
||||
softly.assertThat(meta.getJSON()).isNotNull();
|
||||
|
@ -235,6 +238,8 @@ public class SessionTest {
|
|||
.isThrownBy(meta::getAutoRenewalMinLifetime);
|
||||
softly.assertThatExceptionOfType(AcmeNotSupportedException.class)
|
||||
.isThrownBy(meta::isAutoRenewalGetAllowed);
|
||||
softly.assertThat(meta.isProfileAllowed()).isFalse();
|
||||
softly.assertThat(meta.isProfileAllowed("classic")).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,10 @@
|
|||
"foo",
|
||||
"bar",
|
||||
"barfoo"
|
||||
]
|
||||
],
|
||||
"profiles": {
|
||||
"classic": "The profile you're accustomed to",
|
||||
"custom": "Some other profile"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue