Add support for draft-aaron-acme-profiles

pull/168/head
Jared Crawford 2025-01-09 19:17:32 -05:00
parent 318aeaab9d
commit c7e6d3169c
5 changed files with 172 additions and 1 deletions

View File

@ -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.
*

View File

@ -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());

View File

@ -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.
*/

View File

@ -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();
}
}

View File

@ -23,6 +23,10 @@
"foo",
"bar",
"barfoo"
]
],
"profiles": {
"classic": "The profile you're accustomed to",
"custom": "Some other profile"
}
}
}