org.wildfly.security.x500.cert.acme.AcmeClientSpi Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2018 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.wildfly.security.x500.cert.acme;
import static org.wildfly.security.x500.cert.acme.Acme.ACCEPT_LANGUAGE;
import static org.wildfly.security.x500.cert.acme.Acme.ACCOUNT;
import static org.wildfly.security.x500.cert.acme.Acme.ALG;
import static org.wildfly.security.x500.cert.acme.Acme.AUTHORIZATION;
import static org.wildfly.security.x500.cert.acme.Acme.AUTHORIZATIONS;
import static org.wildfly.security.x500.cert.acme.Acme.BAD_NONCE;
import static org.wildfly.security.x500.cert.acme.Acme.BASE64_URL;
import static org.wildfly.security.x500.cert.acme.Acme.CAA_IDENTITIES;
import static org.wildfly.security.x500.cert.acme.Acme.CERTIFICATE;
import static org.wildfly.security.x500.cert.acme.Acme.CHALLENGES;
import static org.wildfly.security.x500.cert.acme.Acme.CONTACT;
import static org.wildfly.security.x500.cert.acme.Acme.CONTENT_TYPE;
import static org.wildfly.security.x500.cert.acme.Acme.CSR;
import static org.wildfly.security.x500.cert.acme.Acme.DEACTIVATED;
import static org.wildfly.security.x500.cert.acme.Acme.DETAIL;
import static org.wildfly.security.x500.cert.acme.Acme.DNS;
import static org.wildfly.security.x500.cert.acme.Acme.EXTERNAL_ACCOUNT_REQUIRED;
import static org.wildfly.security.x500.cert.acme.Acme.FINALIZE;
import static org.wildfly.security.x500.cert.acme.Acme.GET;
import static org.wildfly.security.x500.cert.acme.Acme.HEAD;
import static org.wildfly.security.x500.cert.acme.Acme.IDENTIFIER;
import static org.wildfly.security.x500.cert.acme.Acme.IDENTIFIERS;
import static org.wildfly.security.x500.cert.acme.Acme.INSTANCE;
import static org.wildfly.security.x500.cert.acme.Acme.INVALID;
import static org.wildfly.security.x500.cert.acme.Acme.JOSE_JSON_CONTENT_TYPE;
import static org.wildfly.security.x500.cert.acme.Acme.JSON_CONTENT_TYPE;
import static org.wildfly.security.x500.cert.acme.Acme.JWK;
import static org.wildfly.security.x500.cert.acme.Acme.KID;
import static org.wildfly.security.x500.cert.acme.Acme.LOCATION;
import static org.wildfly.security.x500.cert.acme.Acme.META;
import static org.wildfly.security.x500.cert.acme.Acme.NONCE;
import static org.wildfly.security.x500.cert.acme.Acme.OLD_KEY;
import static org.wildfly.security.x500.cert.acme.Acme.ONLY_RETURN_EXISTING;
import static org.wildfly.security.x500.cert.acme.Acme.ORDER;
import static org.wildfly.security.x500.cert.acme.Acme.PAYLOAD;
import static org.wildfly.security.x500.cert.acme.Acme.PEM_CERTIFICATE_CHAIN_CONTENT_TYPE;
import static org.wildfly.security.x500.cert.acme.Acme.PENDING;
import static org.wildfly.security.x500.cert.acme.Acme.POST;
import static org.wildfly.security.x500.cert.acme.Acme.PROBLEM_JSON_CONTENT_TYPE;
import static org.wildfly.security.x500.cert.acme.Acme.PROTECTED;
import static org.wildfly.security.x500.cert.acme.Acme.RATE_LIMITED;
import static org.wildfly.security.x500.cert.acme.Acme.REASON;
import static org.wildfly.security.x500.cert.acme.Acme.REPLAY_NONCE;
import static org.wildfly.security.x500.cert.acme.Acme.RETRY_AFTER;
import static org.wildfly.security.x500.cert.acme.Acme.STATUS;
import static org.wildfly.security.x500.cert.acme.Acme.SUBPROBLEMS;
import static org.wildfly.security.x500.cert.acme.Acme.TERMS_OF_SERVICE;
import static org.wildfly.security.x500.cert.acme.Acme.TOKEN;
import static org.wildfly.security.x500.cert.acme.Acme.URL;
import static org.wildfly.security.x500.cert.acme.Acme.SIGNATURE;
import static org.wildfly.security.x500.cert.acme.Acme.TERMS_OF_SERVICE_AGREED;
import static org.wildfly.security.x500.cert.acme.Acme.TITLE;
import static org.wildfly.security.x500.cert.acme.Acme.TYPE;
import static org.wildfly.security.x500.cert.acme.Acme.USER_ACTION_REQUIRED;
import static org.wildfly.security.x500.cert.acme.Acme.USER_AGENT;
import static org.wildfly.security.x500.cert.acme.Acme.VALID;
import static org.wildfly.security.x500.cert.acme.Acme.VALUE;
import static org.wildfly.security.x500.cert.acme.Acme.WEBSITE;
import static org.wildfly.security.x500.cert.acme.Acme.base64UrlEncode;
import static org.wildfly.security.x500.cert.acme.Acme.getAlgHeaderFromSignatureAlgorithm;
import static org.wildfly.security.x500.cert.acme.Acme.getJwk;
import static org.wildfly.security.x500.cert.acme.ElytronMessages.acme;
import static org.wildfly.security.x500.cert.util.KeyUtil.getDefaultCompatibleSignatureAlgorithmName;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonReader;
import jakarta.json.JsonString;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CRLReason;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.security.auth.x500.X500Principal;
import org.wildfly.common.Assert;
import org.wildfly.common.iteration.CodePointIterator;
import org.wildfly.security.Version;
import org.wildfly.security.asn1.ASN1Encodable;
import org.wildfly.security.asn1.DERDecoder;
import org.wildfly.security.x500.GeneralName;
import org.wildfly.security.x500.X500;
import org.wildfly.security.x500.X500AttributeTypeAndValue;
import org.wildfly.security.x500.X500PrincipalBuilder;
import org.wildfly.security.x500.cert.PKCS10CertificateSigningRequest;
import org.wildfly.security.x500.cert.SelfSignedX509CertificateAndSigningKey;
import org.wildfly.security.x500.cert.SubjectAlternativeNamesExtension;
import org.wildfly.security.x500.cert.X509CertificateChainAndSigningKey;
/**
* SPI for an Automatic Certificate Management Environment (ACME)
* client provider to implement.
*
* @author Farah Juma
* @since 1.5.0
*/
public abstract class AcmeClientSpi {
/**
* The default key size that will be used if the key algorithm name is EC.
*/
public static final int DEFAULT_EC_KEY_SIZE = 256;
/**
* The default key size that will be used if the key algorithm name is not EC.
*/
public static final int DEFAULT_KEY_SIZE = 2048;
/**
* The default key algorithm name.
*/
public static final String DEFAULT_KEY_ALGORITHM_NAME = "RSA";
private static final int MAX_RETRIES = 10;
private static final long DEFAULT_RETRY_AFTER_MILLI = 3000;
private static final int[] CONTENT_TYPE_DELIMS = new int[] {';', '='};
private static final String CHARSET = "charset";
private static final String UTF_8 = "utf-8";
private static final String USER_AGENT_STRING = "Elytron ACME Client/" + Version.getVersion();
private static final JsonObject EMPTY_PAYLOAD = Json.createObjectBuilder().build();
private static final String EMPTY_STRING = "";
/**
* Get the resource URLs needed to perform operations from the ACME server.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @return a map of ACME resources to URLs
* @throws AcmeException if an error occurs while attempting to get the resource URLs from the ACME server
*/
public Map getResourceUrls(AcmeAccount account, boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
final Map resourceUrls = account.getResourceUrls(staging);
if (resourceUrls.isEmpty()) {
if (staging && account.getServerUrl(true) == null) {
throw acme.noAcmeServerStagingUrlGiven();
}
HttpURLConnection connection = sendGetRequest(account.getServerUrl(staging), HttpURLConnection.HTTP_OK, JSON_CONTENT_TYPE);
JsonObject directoryJson = getJsonResponse(connection);
try {
for (AcmeResource resource : AcmeResource.values()) {
String resourceUrl = getOptionalJsonString(directoryJson, resource.getValue());
URL url = resourceUrl != null ? new URL(resourceUrl) : null;
resourceUrls.put(resource, url);
}
} catch (MalformedURLException e) {
throw acme.unableToRetrieveAcmeServerDirectoryUrls(e);
}
}
return resourceUrls;
}
/**
* Get the metadata associated with the ACME server.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @return the metadata associated with the ACME server (may be {@code null})
* @throws AcmeException if an error occurs while attempting to get the metadata associated with the ACME server
*/
public AcmeMetadata getMetadata(AcmeAccount account, boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
if (staging && account.getServerUrl(true) == null) {
throw acme.noAcmeServerStagingUrlGiven();
}
HttpURLConnection connection = sendGetRequest(account.getServerUrl(staging), HttpURLConnection.HTTP_OK, JSON_CONTENT_TYPE);
JsonObject directoryJson = getJsonResponse(connection);
JsonObject metadata = directoryJson.getJsonObject(META);
if (metadata == null) {
return null;
}
AcmeMetadata.Builder metadataBuilder = AcmeMetadata.builder();
String termsOfServiceUrl = getOptionalJsonString(metadata, TERMS_OF_SERVICE);
if (termsOfServiceUrl != null) {
metadataBuilder.setTermsOfServiceUrl(termsOfServiceUrl);
}
String websiteUrl = getOptionalJsonString(metadata, WEBSITE);
if (websiteUrl != null) {
metadataBuilder.setWebsiteUrl(websiteUrl);
}
JsonArray caaIdentitiesArray = metadata.getJsonArray(CAA_IDENTITIES);
if (caaIdentitiesArray != null) {
final List caaIdentities = new ArrayList<>(caaIdentitiesArray.size());
for (JsonString caaIdentity : caaIdentitiesArray.getValuesAs(JsonString.class)) {
caaIdentities.add(caaIdentity.getString());
}
metadataBuilder.setCaaIdentities(caaIdentities.toArray(new String[caaIdentities.size()]));
}
boolean externalAccountRequired = metadata.getBoolean(EXTERNAL_ACCOUNT_REQUIRED, false);
metadataBuilder.setExternalAccountRequired(externalAccountRequired);
return metadataBuilder.build();
}
/**
* Create an account with an ACME server using the given account information.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @return {@code true} if the account was created, {@code false} if the account already existed
* @throws AcmeException if an error occurs while attempting to create or lookup an account with
* the ACME server
*/
public boolean createAccount(AcmeAccount account, boolean staging) throws AcmeException {
return createAccount(account, staging, false);
}
/**
* Create an account with an ACME server using the given account information.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param onlyReturnExisting {@code true} if the ACME server should not create a new account if one does not
* already exist (this allows an existing account's URL to be looked up and populated
* using the account key)
* @return {@code true} if the account was created, {@code false} if the account already existed
* @throws AcmeException if an error occurs while attempting to create or lookup an account with the ACME server
* or if {@code onlyReturnExisting} is set to {@code true} and the account does not exist
*/
public boolean createAccount(AcmeAccount account, boolean staging, boolean onlyReturnExisting) throws AcmeException {
Assert.checkNotNullParam("account", account);
final String newAccountUrl = getResourceUrl(account, AcmeResource.NEW_ACCOUNT, staging).toString();
JsonObjectBuilder payloadBuilder = Json.createObjectBuilder();
if (onlyReturnExisting) {
payloadBuilder.add(ONLY_RETURN_EXISTING, true);
} else {
// create a new account
payloadBuilder.add(TERMS_OF_SERVICE_AGREED, account.isTermsOfServiceAgreed());
if (account.getContactUrls() != null && !(account.getContactUrls().length == 0)) {
JsonArrayBuilder contactBuilder = Json.createArrayBuilder();
for (String contactUrl : account.getContactUrls()) {
contactBuilder.add(contactUrl);
}
payloadBuilder.add(CONTACT, contactBuilder.build());
}
}
HttpURLConnection connection = sendPostRequestWithRetries(account, staging, newAccountUrl, true,
getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_OK);
account.setAccountUrl(getLocation(connection, ACCOUNT));
try {
return connection.getResponseCode() == HttpURLConnection.HTTP_CREATED;
} catch (IOException e) {
throw new AcmeException(e);
}
}
/**
* Update whether or not the terms of service have been agreed to for an account with an ACME server.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param termsOfServiceAgreed the new value for whether or not the terms of service have been agreed to
* @throws AcmeException if an error occurs while attempting to update the account
*/
public void updateAccount(AcmeAccount account, boolean staging, boolean termsOfServiceAgreed) throws AcmeException {
updateAccount(account, staging, termsOfServiceAgreed, null);
}
/**
* Update the contact URLs for an account with an ACME server.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param contactUrls the new account contact URLs
* @throws AcmeException if an error occurs while attempting to update the account
*/
public void updateAccount(AcmeAccount account, boolean staging, String[] contactUrls) throws AcmeException {
updateAccount(account, staging, account.isTermsOfServiceAgreed(), contactUrls);
}
/**
* Update an account with an ACME server using the given account information.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param termsOfServiceAgreed the new value for whether or not the terms of service have been agreed to
* @param contactUrls the new account contact URLs
* @throws AcmeException if an error occurs while attempting to update the account
*/
public void updateAccount(AcmeAccount account, boolean staging, boolean termsOfServiceAgreed, String[] contactUrls) throws AcmeException {
Assert.checkNotNullParam("account", account);
JsonObjectBuilder payloadBuilder = Json.createObjectBuilder()
.add(TERMS_OF_SERVICE_AGREED, termsOfServiceAgreed);
if (contactUrls != null && ! (contactUrls.length == 0)) {
JsonArrayBuilder contactBuilder = Json.createArrayBuilder();
for (String contactUrl : contactUrls) {
contactBuilder.add(contactUrl);
}
payloadBuilder.add(CONTACT, contactBuilder.build());
}
sendPostRequestWithRetries(account, staging, getAccountUrl(account, staging), false,
getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_OK);
account.setTermsOfServiceAgreed(termsOfServiceAgreed);
if (contactUrls != null && ! (contactUrls.length == 0)) {
account.setContactUrls(contactUrls);
}
}
/**
* Change the key that is associated with the given ACME account.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @throws AcmeException if an error occurs while attempting to change the key that is associated with the given ACME account
*/
public void changeAccountKey(AcmeAccount account, boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
SelfSignedX509CertificateAndSigningKey newCertificateAndSigningKey = SelfSignedX509CertificateAndSigningKey.builder()
.setKeySize(account.getKeySize())
.setKeyAlgorithmName(account.getKeyAlgorithmName())
.setDn(account.getDn())
.build();
changeAccountKey(account, staging, newCertificateAndSigningKey.getSelfSignedCertificate(), newCertificateAndSigningKey.getSigningKey());
}
/**
* Change the key that is associated with the given ACME account.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param certificate the new certificate to associate with the given ACME account (must not be {@code null})
* @param privateKey the new private key to associate with the given ACME account (must not be {@code null})
* @throws AcmeException if an error occurs while attempting to change the key that is associated with the given ACME account
*/
public void changeAccountKey(AcmeAccount account, boolean staging, X509Certificate certificate, PrivateKey privateKey) throws AcmeException {
Assert.checkNotNullParam("account", account);
Assert.checkNotNullParam("certificate", certificate);
Assert.checkNotNullParam("privateKey", privateKey);
final String keyChangeUrl = getResourceUrl(account, AcmeResource.KEY_CHANGE, staging).toString();
final String signatureAlgorithm = getDefaultCompatibleSignatureAlgorithmName(privateKey);
final String algHeader = getAlgHeaderFromSignatureAlgorithm(signatureAlgorithm);
final String innerEncodedProtectedHeader = getEncodedProtectedHeader(algHeader, certificate.getPublicKey(), keyChangeUrl);
JsonObjectBuilder innerPayloadBuilder = Json.createObjectBuilder()
.add(ACCOUNT, getAccountUrl(account, staging))
.add(OLD_KEY, getJwk(account.getPublicKey(), account.getAlgHeader()));
final String innerEncodedPayload = getEncodedJson(innerPayloadBuilder.build());
final String innerEncodedSignature = getEncodedSignature(privateKey, signatureAlgorithm, innerEncodedProtectedHeader, innerEncodedPayload);
final String outerEncodedPayload = getEncodedJson(getJws(innerEncodedProtectedHeader, innerEncodedPayload, innerEncodedSignature));
sendPostRequestWithRetries(account, staging, keyChangeUrl, false, outerEncodedPayload, HttpURLConnection.HTTP_OK);
account.changeCertificateAndPrivateKey(certificate, privateKey); // update account info
}
/**
* Deactivate the given ACME account. It is not possible to reactivate an ACME account after it has
* been deactivated.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @throws AcmeException if an error occurs while attempting to deactivate the given ACME account
*/
public void deactivateAccount(AcmeAccount account, boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
JsonObject payload = Json.createObjectBuilder()
.add(STATUS, DEACTIVATED)
.build();
sendPostRequestWithRetries(account, staging, getAccountUrl(account, staging), false, getEncodedJson(payload), HttpURLConnection.HTTP_OK);
}
/**
* Obtain a certificate chain using the given ACME account.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param domainNames the domain names to request the certificate for (must not be {@code null})
* @return the X509 certificate chain and private key
* @throws AcmeException if an occur occurs while attempting to obtain the certificate
*/
public X509CertificateChainAndSigningKey obtainCertificateChain(AcmeAccount account, boolean staging, String... domainNames) throws AcmeException {
return obtainCertificateChain(account, staging, null, -1, domainNames);
}
/**
* Obtain a certificate chain using the given ACME account.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param keyAlgorithmName the optional key algorithm name to use when generating the key pair (may be {@code null})
* @param keySize the optional key size to use when generating the key pair (-1 to indicate that the default key size should be used)
* @param domainNames the domain names to request the certificate for (must not be {@code null})
* @return the X509 certificate chain and private key
* @throws AcmeException if an occur occurs while attempting to obtain the certificate
*/
public X509CertificateChainAndSigningKey obtainCertificateChain(AcmeAccount account, boolean staging, String keyAlgorithmName, int keySize,
String... domainNames) throws AcmeException {
Assert.checkNotNullParam("account", account);
Assert.checkNotNullParam("domainNames", domainNames);
final LinkedHashSet domainNamesSet = getDomainNames(domainNames);
// create a new order
final String newOrderUrl = getResourceUrl(account, AcmeResource.NEW_ORDER, staging).toString();
JsonArrayBuilder identifiersBuilder = Json.createArrayBuilder();
for (String domainName : domainNamesSet) {
JsonObject identifier = Json.createObjectBuilder()
.add(TYPE, DNS)
.add(VALUE, domainName)
.build();
identifiersBuilder.add(identifier);
}
JsonObjectBuilder payloadBuilder = Json.createObjectBuilder()
.add(IDENTIFIERS, identifiersBuilder.build());
HttpURLConnection connection = sendPostRequestWithRetries(account, staging, newOrderUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_CREATED);
final String orderUrl = getLocation(connection, ORDER);
JsonObject jsonResponse = getJsonResponse(connection);
final String finalizeOrderUrl = jsonResponse.getString(FINALIZE);
final JsonArray authorizationsArray = jsonResponse.getJsonArray(AUTHORIZATIONS);
final List authorizationUrls = new ArrayList<>(authorizationsArray.size());
for (JsonString authorization : authorizationsArray.getValuesAs(JsonString.class)) {
authorizationUrls.add(authorization.getString());
}
// respond to challenges for each authorization resource
List selectedChallenges = new ArrayList<>(authorizationUrls.size());
try {
for (String authorizationUrl : authorizationUrls) {
connection = sendPostAsGetRequest(account, staging, authorizationUrl, JSON_CONTENT_TYPE, HttpURLConnection.HTTP_OK);
jsonResponse = getJsonResponse(connection);
AcmeChallenge selectedChallenge = respondToChallenges(account, staging, jsonResponse);
if (selectedChallenge != null) {
selectedChallenges.add(selectedChallenge);
}
}
// poll the authorization resources until server has finished validating the challenge responses
for (String authorizationUrl : authorizationUrls) {
jsonResponse = pollResourceUntilFinalized(account, staging, authorizationUrl);
if (! jsonResponse.getString(STATUS).equals(VALID)) {
throw acme.challengeResponseFailedValidationByAcmeServer();
}
}
// create and submit a CSR now that we've fulfilled the server's requirements
List generalNames = new ArrayList<>(domainNamesSet.size());
for (String domainName : domainNamesSet) {
generalNames.add(new GeneralName.DNSName(domainName));
}
X500PrincipalBuilder principalBuilder = new X500PrincipalBuilder();
principalBuilder.addItem(X500AttributeTypeAndValue.create(X500.OID_AT_COMMON_NAME, ASN1Encodable.ofUtf8String(((GeneralName.DNSName) generalNames.get(0)).getName())));
X500Principal dn = principalBuilder.build();
if (keyAlgorithmName == null) {
keyAlgorithmName = DEFAULT_KEY_ALGORITHM_NAME;
}
if (keySize == -1) {
if (keyAlgorithmName.equals("EC")) {
keySize = DEFAULT_EC_KEY_SIZE;
} else {
keySize = DEFAULT_KEY_SIZE;
}
}
SelfSignedX509CertificateAndSigningKey selfSignedX509CertificateAndSigningKey = SelfSignedX509CertificateAndSigningKey.builder()
.setDn(dn)
.setKeyAlgorithmName(keyAlgorithmName)
.setKeySize(keySize)
.build();
PKCS10CertificateSigningRequest.Builder csrBuilder = PKCS10CertificateSigningRequest.builder()
.setCertificate(selfSignedX509CertificateAndSigningKey.getSelfSignedCertificate())
.setSigningKey(selfSignedX509CertificateAndSigningKey.getSigningKey())
.setSubjectDn(dn);
csrBuilder.addExtension(new SubjectAlternativeNamesExtension(false, generalNames));
payloadBuilder = Json.createObjectBuilder()
.add(CSR, base64UrlEncode(csrBuilder.build().getEncoded()));
connection = sendPostRequestWithRetries(account, staging, finalizeOrderUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_OK);
// poll the order resource until the server has made the certificate chain available
jsonResponse = pollResourceUntilFinalized(account, staging, orderUrl);
if (! jsonResponse.getString(STATUS).equals(VALID)) {
throw acme.noCertificateWillBeIssuedByAcmeServer();
}
// download the certificate chain
String certificateUrl = getOptionalJsonString(jsonResponse, CERTIFICATE);
if (certificateUrl == null) {
throw acme.noCertificateUrlProvidedByAcmeServer();
}
connection = sendPostAsGetRequest(account, staging, certificateUrl, PEM_CERTIFICATE_CHAIN_CONTENT_TYPE, HttpURLConnection.HTTP_OK);
X509Certificate[] certificateChain = getPemCertificateChain(connection);
PrivateKey privateKey = selfSignedX509CertificateAndSigningKey.getSigningKey();
return new X509CertificateChainAndSigningKey(certificateChain, privateKey);
} finally {
// clean up
for (AcmeChallenge challenge : selectedChallenges) {
cleanupAfterChallenge(account, challenge);
}
}
}
/**
* Create an authorization for the given identifier.
*
* This method allows an ACME client to obtain authorization for an identifier proactively before attempting
* to obtain a certificate.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param domainName the domain name to create an authorization for (must not be {@code null})
* @return the authorization URL corresponding to the given identifier
* @throws AcmeException if an error occurs while attempting to create an authorization for the given identifier
*/
public String createAuthorization(AcmeAccount account, boolean staging, String domainName) throws AcmeException {
Assert.checkNotNullParam("account", account);
Assert.checkNotNullParam("domainName", domainName);
final String newAuthzUrl = getResourceUrl(account, AcmeResource.NEW_AUTHZ, staging).toString();
JsonObject identifier = Json.createObjectBuilder()
.add(TYPE, DNS)
.add(VALUE, getSanitizedDomainName(domainName))
.build();
JsonObjectBuilder payloadBuilder = Json.createObjectBuilder()
.add(IDENTIFIER, identifier);
HttpURLConnection connection = sendPostRequestWithRetries(account, staging, newAuthzUrl, false,
getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_CREATED);
String authorizationUrl = getLocation(connection, AUTHORIZATION);
JsonObject jsonResponse = getJsonResponse(connection);
AcmeChallenge selectedChallenge = respondToChallenges(account, staging, jsonResponse);
try {
jsonResponse = pollResourceUntilFinalized(account, staging, authorizationUrl);
if (! jsonResponse.getString(STATUS).equals(VALID)) {
throw acme.challengeResponseFailedValidationByAcmeServer();
}
return authorizationUrl;
} finally {
if (selectedChallenge != null) {
cleanupAfterChallenge(account, selectedChallenge);
}
}
}
/**
* Deactivate an authorization.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param authorizationUrl the authorization url (must not be {@code null})
* @throws AcmeException if an error occurs while attempting to deactivate an authorization for the given identifier
*/
public void deactivateAuthorization(AcmeAccount account, boolean staging, String authorizationUrl) throws AcmeException {
Assert.checkNotNullParam("account", account);
Assert.checkNotNullParam("authorizationUrl", authorizationUrl);
JsonObject payload = Json.createObjectBuilder()
.add(STATUS, DEACTIVATED)
.build();
sendPostRequestWithRetries(account, staging, authorizationUrl, false, getEncodedJson(payload), HttpURLConnection.HTTP_OK);
}
/**
* Prove control of the identifier associated with the given list of challenges.
*
* This method should select one challenge from the given list of challenges from the ACME server to prove
* control of the identifier associated with the challenges as specified by the ACME v2 protocol.
*
* @param account the ACME account information to use (must not be {@code null})
* @param challenges the list of challenges from the ACME server (must not be {@code null})
* @return the challenge that was selected and used to prove control of the identifier
* @throws AcmeException if an error occurs while attempting to provide control of the identifier associated
* with the challenges or if none of the challenge types are supported by this client
*/
public abstract AcmeChallenge proveIdentifierControl(AcmeAccount account, List challenges) throws AcmeException;
/**
* Undo the actions that were taken to prove control of the identifier associated with the given challenge.
*
* @param account the ACME account information to use (must not be {@code null})
* @param challenge the challenge (must not be {@code null})
* @throws AcmeException if an error occurs while attempting to undo the actions that were taken to prove control
* of the identifier associated with the given challenge
*/
public abstract void cleanupAfterChallenge(AcmeAccount account, AcmeChallenge challenge) throws AcmeException;
/**
* Revoke the given certificate.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param certificate the certificate to be revoked (must not be {@code null})
* @throws AcmeException if an error occurs while attempting to revoke the given certificate
*/
public void revokeCertificate(AcmeAccount account, boolean staging, X509Certificate certificate) throws AcmeException {
revokeCertificate(account, staging, certificate, null);
}
/**
* Revoke the given certificate.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @param certificate the certificate to be revoked (must not be {@code null})
* @param reason the optional reason why the certificate is being revoked (may be {@code null})
* @throws AcmeException if an error occurs while attempting to revoke the given certificate
*/
public void revokeCertificate(AcmeAccount account, boolean staging, X509Certificate certificate, CRLReason reason) throws AcmeException {
Assert.checkNotNullParam("account", account);
Assert.checkNotNullParam("certificate", certificate);
final String revokeCertUrl = getResourceUrl(account, AcmeResource.REVOKE_CERT, staging).toString();
byte[] encodedCertificate;
try {
encodedCertificate = certificate.getEncoded();
} catch (CertificateEncodingException e) {
throw acme.unableToGetEncodedFormOfCertificateToBeRevoked(e);
}
JsonObjectBuilder payloadBuilder = Json.createObjectBuilder()
.add(CERTIFICATE, base64UrlEncode(encodedCertificate));
if (reason != null) {
payloadBuilder.add(REASON, reason.ordinal());
}
sendPostRequestWithRetries(account, staging, revokeCertUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_OK);
}
/**
* Get a new nonce for the given account from the ACME server.
*
* @param account the ACME account information to use (must not be {@code null})
* @param staging whether or not the staging server URL should be used
* @return nonce the new nonce for the given account
* @throws AcmeException if an error occurs while attempting to get the new nonce from the ACME server
*/
public byte[] getNewNonce(final AcmeAccount account, final boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
try {
final URL newNonceUrl = getResourceUrl(account, AcmeResource.NEW_NONCE, staging);
HttpURLConnection connection = (HttpURLConnection) newNonceUrl.openConnection();
connection.setRequestMethod(HEAD);
connection.setRequestProperty(ACCEPT_LANGUAGE, Locale.getDefault().toLanguageTag());
connection.setRequestProperty(USER_AGENT, USER_AGENT_STRING);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_NO_CONTENT && responseCode != HttpURLConnection.HTTP_OK) {
handleAcmeErrorResponse(connection, responseCode);
}
byte[] nonce = getReplayNonce(connection);
if (nonce == null) {
throw acme.noNonceProvidedByAcmeServer();
}
return nonce;
} catch (Exception e) {
throw acme.unableToObtainNewNonceFromAcmeServer();
}
}
String[] queryAccountContactUrls(AcmeAccount account, boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
HttpURLConnection connection = sendPostAsGetRequest(account, staging, getAccountUrl(account, staging), null, HttpURLConnection.HTTP_OK);
JsonObject jsonResponse = getJsonResponse(connection);
JsonArray contactsArray = jsonResponse.getJsonArray(CONTACT);
if (contactsArray != null && !contactsArray.isEmpty()) {
List contacts = new ArrayList<>(contactsArray.size());
for (JsonString contact : contactsArray.getValuesAs(JsonString.class)) {
contacts.add(contact.getString());
}
return contacts.toArray(new String[contacts.size()]);
}
return null;
}
String queryAccountStatus(AcmeAccount account, boolean staging) throws AcmeException {
Assert.checkNotNullParam("account", account);
HttpURLConnection connection = sendPostAsGetRequest(account, staging, getAccountUrl(account, staging), null, HttpURLConnection.HTTP_OK);
JsonObject jsonResponse = getJsonResponse(connection);
return jsonResponse.getString(STATUS);
}
private URL getResourceUrl(AcmeAccount account, AcmeResource resource, boolean staging) throws AcmeException {
URL resourceUrl = getResourceUrls(account, staging).get(resource);
if (resourceUrl == null) {
throw acme.resourceNotSupportedByAcmeServer(resource.getValue());
}
return resourceUrl;
}
private HttpURLConnection sendGetRequest(String resourceUrl, int expectedResponseCode, String expectedContentType) throws AcmeException {
try {
final URL directoryUrl = new URL(resourceUrl);
HttpURLConnection connection = (HttpURLConnection) directoryUrl.openConnection();
connection.setRequestMethod(GET);
connection.setRequestProperty(ACCEPT_LANGUAGE, Locale.getDefault().toLanguageTag());
connection.setRequestProperty(USER_AGENT, USER_AGENT_STRING);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode != expectedResponseCode) {
handleAcmeErrorResponse(connection, responseCode);
}
String contentType = connection.getContentType();
if (! checkContentType(connection, expectedContentType)) {
throw acme.unexpectedContentTypeFromAcmeServer(contentType);
}
return connection;
} catch (Exception e) {
if (e instanceof AcmeException) {
throw (AcmeException) e;
} else {
throw new AcmeException(e);
}
}
}
private HttpURLConnection sendPostAsGetRequest(AcmeAccount account, boolean staging, String resourceUrl,
String expectedContentType, int... expectedResponseCodes) throws AcmeException {
// payload of the JWS must be a zero-length octet string
return sendPostRequestWithRetries(account, staging, resourceUrl, false, EMPTY_STRING,
expectedContentType, expectedResponseCodes);
}
private HttpURLConnection sendPostRequestWithRetries(AcmeAccount account, boolean staging, String resourceUrl, boolean useJwk, String encodedPayload,
int... expectedResponseCodes) throws AcmeException {
return sendPostRequestWithRetries(account, staging, resourceUrl, useJwk, encodedPayload, null, expectedResponseCodes);
}
private HttpURLConnection sendPostRequestWithRetries(AcmeAccount account, boolean staging, String resourceUrl, boolean useJwk, String encodedPayload,
String expectedContentType, int... expectedResponseCodes) throws AcmeException {
try {
final URL url = new URL(resourceUrl);
HttpURLConnection connection;
for (int i = 0; i < MAX_RETRIES; i++) {
String encodedProtectedHeader = getEncodedProtectedHeader(useJwk, resourceUrl, account, staging);
String encodedSignature = getEncodedSignature(account.getPrivateKey(), account.getSignature(), encodedProtectedHeader, encodedPayload);
JsonObject jws = getJws(encodedProtectedHeader, encodedPayload, encodedSignature);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(POST);
connection.setRequestProperty(CONTENT_TYPE, JOSE_JSON_CONTENT_TYPE);
connection.setRequestProperty(ACCEPT_LANGUAGE, Locale.getDefault().toLanguageTag());
connection.setRequestProperty(USER_AGENT, USER_AGENT_STRING);
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(jws.toString().length());
connection.connect();
try (OutputStream out = connection.getOutputStream()) {
out.write(jws.toString().getBytes(StandardCharsets.US_ASCII));
}
int responseCode = connection.getResponseCode();
account.setNonce(getReplayNonce(connection)); // update the account nonce
for (int expectedResponseCode : expectedResponseCodes) {
if (expectedResponseCode == responseCode) {
if (expectedContentType != null) {
String contentType = connection.getContentType();
if (! checkContentType(connection, expectedContentType)) {
throw acme.unexpectedContentTypeFromAcmeServer(contentType);
}
}
return connection;
}
}
handleAcmeErrorResponse(connection, responseCode);
}
throw acme.badAcmeNonce(); // max attempts reached
} catch (Exception e) {
if (e instanceof AcmeException) {
throw (AcmeException) e;
} else {
throw new AcmeException(e);
}
}
}
private JsonObject pollResourceUntilFinalized(AcmeAccount account, boolean staging, String resourceUrl) throws AcmeException {
boolean statusFinalized;
JsonObject jsonResponse;
do {
statusFinalized = true;
HttpURLConnection connection = sendPostAsGetRequest(account, staging, resourceUrl, JSON_CONTENT_TYPE, HttpURLConnection.HTTP_OK);
jsonResponse = getJsonResponse(connection);
String status = jsonResponse.getString(STATUS);
if (! status.equals(VALID) && ! status.equals(INVALID)) {
// server still processing the client response, try again after some time
statusFinalized = false;
long retryAfterMilli = getRetryAfter(connection, true);
if (retryAfterMilli > 0) {
try {
Thread.sleep(retryAfterMilli);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
} while (! statusFinalized);
return jsonResponse;
}
private AcmeChallenge respondToChallenges(AcmeAccount account, boolean staging, JsonObject authorization) throws AcmeException {
List challenges = null;
if (authorization.getString(STATUS).equals(PENDING)) {
JsonObject identifier = authorization.getJsonObject(IDENTIFIER);
JsonArray challengeArray = authorization.getJsonArray(CHALLENGES);
challenges = new ArrayList<>(challengeArray.size());
for (JsonObject challenge : challengeArray.getValuesAs(JsonObject.class)) {
challenges.add(new AcmeChallenge(AcmeChallenge.Type.forName(challenge.getString(TYPE)), challenge.getString(URL),
challenge.getString(TOKEN), identifier.getString(TYPE), identifier.getString(VALUE)));
}
}
if (challenges != null && ! challenges.isEmpty()) {
AcmeChallenge selectedChallenge = proveIdentifierControl(account, challenges);
try {
sendPostRequestWithRetries(account, staging, selectedChallenge.getUrl(), false, getEncodedJson(EMPTY_PAYLOAD), HttpURLConnection.HTTP_OK);
return selectedChallenge;
} catch (AcmeException e) {
cleanupAfterChallenge(account, selectedChallenge);
throw e;
}
}
return null;
}
private static LinkedHashSet getDomainNames(String[] domainNames) throws AcmeException {
if (domainNames.length == 0) {
throw acme.domainNamesIsEmpty();
}
final LinkedHashSet domainNamesSet = new LinkedHashSet<>();
for (String domainName : domainNames) {
domainNamesSet.add(getSanitizedDomainName(domainName));
}
return domainNamesSet;
}
private static String getSanitizedDomainName(String domainName) throws AcmeException {
if (domainName == null) {
throw acme.domainNameIsNull();
}
domainName = IDN.toASCII(domainName.trim());
return domainName.toLowerCase(Locale.ROOT);
}
/* -- Methods used to parse responses from the ACME server -- */
private static JsonObject getJsonResponse(HttpURLConnection connection) throws AcmeException {
JsonObject jsonResponse;
try (InputStream inputStream = new BufferedInputStream(connection.getResponseCode() < 400 ? connection.getInputStream() : connection.getErrorStream());
JsonReader jsonReader = Json.createReader(inputStream)) {
jsonResponse = jsonReader.readObject();
} catch (IOException e) {
throw acme.unableToObtainJsonResponseFromAcmeServer(e);
}
return jsonResponse;
}
private static byte[] getReplayNonce(HttpURLConnection connection) throws AcmeException {
String nonce = connection.getHeaderField(REPLAY_NONCE);
if (nonce == null) {
return null;
}
return CodePointIterator.ofString(nonce).base64Decode(BASE64_URL, false).drain();
}
private static String getLocation(HttpURLConnection connection, String urlType) throws AcmeException {
String location = connection.getHeaderField(LOCATION);
if (location == null) {
throw acme.noLocationUrlProvidedByAcmeServer(urlType);
}
return location;
}
private static long getRetryAfter(HttpURLConnection connection, boolean useDefaultIfHeaderNotPresent) throws AcmeException {
long retryAfterMilli = -1;
String retryAfter = connection.getHeaderField(RETRY_AFTER);
if (retryAfter != null) {
try {
retryAfterMilli = Integer.parseInt(retryAfter) * 1000;
} catch (NumberFormatException e) {
long retryAfterDate = connection.getHeaderFieldDate(RETRY_AFTER, 0L);
if (retryAfterDate != 0) {
retryAfterMilli = retryAfterDate - Instant.now().toEpochMilli();
}
}
}
if (retryAfterMilli == -1 && useDefaultIfHeaderNotPresent) {
retryAfterMilli = DEFAULT_RETRY_AFTER_MILLI;
}
return retryAfterMilli;
}
private static void handleAcmeErrorResponse(HttpURLConnection connection, int responseCode) throws AcmeException {
try {
String responseMessage = connection.getResponseMessage();
if (! checkContentType(connection, PROBLEM_JSON_CONTENT_TYPE)) {
throw acme.unexpectedResponseCodeFromAcmeServer(responseCode, responseMessage);
}
JsonObject jsonResponse = getJsonResponse(connection);
String type = getOptionalJsonString(jsonResponse, TYPE);
if (type != null) {
if (type.equals(BAD_NONCE)) {
return; // the request will be re-attempted
} else if (type.equals(USER_ACTION_REQUIRED)) {
String instance = getOptionalJsonString(jsonResponse, INSTANCE);
if (instance != null) {
throw acme.userActionRequired(instance);
}
} else if (type.equals(RATE_LIMITED)) {
long retryAfter = getRetryAfter(connection, false);
if (retryAfter > 0) {
throw acme.rateLimitExceededTryAgainLater(Instant.ofEpochMilli(retryAfter));
} else {
throw acme.rateLimitExceeded();
}
}
}
String problemMessages = getProblemMessages(jsonResponse);
if (problemMessages != null && ! problemMessages.isEmpty()) {
throw new AcmeException(problemMessages);
} else {
throw acme.unexpectedResponseCodeFromAcmeServer(responseCode, responseMessage);
}
} catch (Exception e) {
if (e instanceof AcmeException) {
throw (AcmeException) e;
} else {
throw new AcmeException(e);
}
}
}
private static String getProblemMessages(JsonObject errorResponse) {
StringBuilder problemMessages = new StringBuilder();
String mainProblem = getProblemMessage(errorResponse);
if (mainProblem != null) {
problemMessages.append(getProblemMessage(errorResponse));
}
JsonArray subproblems = errorResponse.getJsonArray(SUBPROBLEMS);
if (subproblems != null && !subproblems.isEmpty()) {
problemMessages.append(":");
for (JsonObject subproblem : subproblems.getValuesAs(JsonObject.class)) {
problemMessages.append("\n").append(getProblemMessage(subproblem));
}
}
return problemMessages.toString();
}
private static String getProblemMessage(JsonObject jsonResponse) {
String type = getOptionalJsonString(jsonResponse, TYPE);
String detail = getOptionalJsonString(jsonResponse, DETAIL);
String title = getOptionalJsonString(jsonResponse, TITLE);
String problemMessage = null;
if (detail != null) {
problemMessage = detail;
} else if (title != null) {
problemMessage = title;
} else if (type != null) {
problemMessage = type;
}
return problemMessage;
}
private static String getOptionalJsonString(JsonObject jsonObject, String name) {
JsonString value = jsonObject.getJsonString(name);
if (value == null) {
return null;
}
return value.getString();
}
private static X509Certificate[] getPemCertificateChain(HttpURLConnection connection) throws AcmeException {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection extends Certificate> reply;
try (InputStream inputStream = new BufferedInputStream(getConvertedInputStream(connection.getInputStream()))) {
reply = certificateFactory.generateCertificates(inputStream);
}
return X500.asX509CertificateArray(reply.toArray(new Certificate[reply.size()]));
} catch (CertificateException | IOException e) {
throw acme.unableToDownloadCertificateChainFromAcmeServer(e);
}
}
/* -- Methods used to encode JWS messages to send to the ACME server -- */
private static String getEncodedJson(JsonObject jsonObject) {
return CodePointIterator.ofString(jsonObject.toString()).asUtf8().base64Encode(BASE64_URL, false).drainToString();
}
private static JsonObject getJws(String encodedProtectedHeader, String encodedPayload, String encodedSignature) {
return Json.createObjectBuilder()
.add(PROTECTED, encodedProtectedHeader)
.add(PAYLOAD, encodedPayload)
.add(SIGNATURE, encodedSignature)
.build();
}
private static String getEncodedProtectedHeader(String algHeader, PublicKey publicKey, String resourceUrl) {
JsonObject protectedHeader = Json.createObjectBuilder()
.add(ALG, algHeader)
.add(JWK, getJwk(publicKey, algHeader))
.add(URL, resourceUrl)
.build();
return getEncodedJson(protectedHeader);
}
private String getEncodedProtectedHeader(boolean useJwk, String resourceUrl, AcmeAccount account, boolean staging) throws AcmeException {
JsonObjectBuilder protectedHeaderBuilder = Json.createObjectBuilder().add(ALG, account.getAlgHeader());
if (useJwk) {
protectedHeaderBuilder.add(JWK, getJwk(account.getPublicKey(), account.getAlgHeader()));
} else {
protectedHeaderBuilder.add(KID, getAccountUrl(account, staging));
}
protectedHeaderBuilder
.add(NONCE, base64UrlEncode(getNonce(account, staging)))
.add(URL, resourceUrl);
return getEncodedJson(protectedHeaderBuilder.build());
}
private static String getEncodedSignature(PrivateKey privateKey, Signature signature, String encodedProtectedHeader, String encodedPayload) throws AcmeException {
final byte[] signatureBytes;
try {
signature.update((encodedProtectedHeader + "." + encodedPayload).getBytes(StandardCharsets.UTF_8));
signatureBytes = signature.sign();
if (privateKey instanceof ECPrivateKey) {
// need to convert the DER encoded signature to concatenated bytes
DERDecoder derDecoder = new DERDecoder(signatureBytes);
derDecoder.startSequence();
byte[] r = derDecoder.drainElementValue();
byte[] s = derDecoder.drainElementValue();
derDecoder.endSequence();
int rLength = r.length;
int sLength = s.length;
int rActual = rLength;
int sActual = sLength;
while (rActual > 0 && r[rLength - rActual] == 0) {
rActual--;
}
while (sActual > 0 && s[sLength - sActual] == 0) {
sActual--;
}
int rawLength = Math.max(rActual, sActual);
int signatureByteLength = getECSignatureByteLength(signature.getAlgorithm());
rawLength = Math.max(rawLength, signatureByteLength / 2);
byte[] concatenatedSignatureBytes = new byte[rawLength * 2];
System.arraycopy(r, rLength - rActual, concatenatedSignatureBytes, rawLength - rActual, rActual);
System.arraycopy(s, sLength - sActual, concatenatedSignatureBytes, 2 * rawLength - sActual, sActual);
return base64UrlEncode(concatenatedSignatureBytes);
}
return base64UrlEncode(signatureBytes);
} catch (SignatureException e) {
throw acme.unableToCreateAcmeSignature(e);
}
}
private static String getEncodedSignature(PrivateKey privateKey, String signatureAlgorithm, String encodedProtectedHeader, String encodedPayload) throws AcmeException {
try {
Signature signature = Signature.getInstance(signatureAlgorithm);
signature.initSign(privateKey);
return getEncodedSignature(privateKey, signature, encodedProtectedHeader, encodedPayload);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw acme.unableToCreateAcmeSignature(e);
}
}
private static int getECSignatureByteLength(String signatureAlgorithm) throws AcmeException {
switch(signatureAlgorithm) {
case "SHA256withECDSA":
return 64;
case "SHA384withECDSA":
return 96;
case "SHA512withECDSA":
return 132;
default:
throw acme.unsupportedAcmeAccountSignatureAlgorithm(signatureAlgorithm);
}
}
private byte[] getNonce(AcmeAccount account, boolean staging) throws AcmeException {
byte[] nonce = account.getNonce();
if (nonce == null) {
nonce = getNewNonce(account, staging);
}
return nonce;
}
private String getAccountUrl(AcmeAccount account, boolean staging) throws AcmeException {
String accountUrl = account.getAccountUrl();
if (accountUrl == null) {
createAccount(account, staging, true);
accountUrl = account.getAccountUrl();
if (accountUrl == null) {
acme.acmeAccountDoesNotExist();
}
}
return accountUrl;
}
private static boolean checkContentType(HttpURLConnection connection, String expectedMediaType) throws AcmeException {
String contentType = connection.getContentType();
if (contentType == null) {
return false;
}
CodePointIterator cpi = CodePointIterator.ofString(contentType);
CodePointIterator di = cpi.delimitedBy(CONTENT_TYPE_DELIMS);
String mediaType = di.drainToString().trim();
skipDelims(di, cpi, CONTENT_TYPE_DELIMS);
while (di.hasNext()) {
String parameter = di.drainToString().trim();
skipDelims(di, cpi, CONTENT_TYPE_DELIMS);
if (parameter.equalsIgnoreCase(CHARSET)) {
String value = di.drainToString().trim();
if (! value.equalsIgnoreCase(UTF_8)) {
return false;
}
}
}
return mediaType.equalsIgnoreCase(expectedMediaType);
}
private static void skipDelims(CodePointIterator di, CodePointIterator cpi, int...delims) throws AcmeException {
while ((! di.hasNext()) && cpi.hasNext()) {
if (! isDelim(cpi.next(), delims)) {
throw acme.invalidContentTypeFromAcmeServer();
}
}
}
private static boolean isDelim(int c, int... delims) {
for (int delim : delims) {
if (delim == c) {
return true;
}
}
return false;
}
private static InputStream getConvertedInputStream(InputStream inputStream) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String currentLine;
while ((currentLine = reader.readLine()) != null) {
// ignore any blank lines to avoid parsing issues on IBM JDK
if (! currentLine.trim().isEmpty()) {
sb.append(currentLine + System.lineSeparator());
}
}
}
return new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8));
}
}