/** * 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); }
/** * 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); }
/** * 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); }
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); }
/** * 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); }
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()); }
Assert.checkNotNullParam("account", account); Assert.checkNotNullParam("domainNames", domainNames); final LinkedHashSet<String> domainNamesSet = getDomainNames(domainNames); final String newOrderUrl = getResourceUrl(account, AcmeResource.NEW_ORDER, staging).toString(); JsonArrayBuilder identifiersBuilder = Json.createArrayBuilder(); for (String domainName : domainNamesSet) { HttpURLConnection connection = sendPostRequestWithRetries(account, staging, newOrderUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_CREATED); JsonObject jsonResponse = getJsonResponse(connection); final String finalizeOrderUrl = jsonResponse.getString(FINALIZE); final JsonArray authorizationsArray = jsonResponse.getJsonArray(AUTHORIZATIONS); 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); jsonResponse = pollResourceUntilFinalized(account, staging, authorizationUrl); if (! jsonResponse.getString(STATUS).equals(VALID)) { throw acme.challengeResponseFailedValidationByAcmeServer(); connection = sendPostRequestWithRetries(account, staging, finalizeOrderUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_OK); final String orderUrl = getLocation(connection); jsonResponse = pollResourceUntilFinalized(account, staging, orderUrl);
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); 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(); } finally { if (selectedChallenge != null) { cleanupAfterChallenge(account, selectedChallenge);
/** * 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 }
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); long retryAfter = getRetryAfter(connection, false); if (retryAfter > 0) { throw acme.rateLimitExceededTryAgainLater(Instant.ofEpochMilli(retryAfter)); String problemMessages = getProblemMessages(jsonResponse); if (problemMessages != null && ! problemMessages.isEmpty()) { throw new AcmeException(problemMessages);
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); account.setNonce(getReplayNonce(connection)); // update the account nonce if (expectedContentType != null) { String contentType = connection.getContentType(); if (! checkContentType(connection, expectedContentType)) { throw acme.unexpectedContentTypeFromAcmeServer(contentType); handleAcmeErrorResponse(connection, responseCode);
private AcmeChallenge respondToChallenges(AcmeAccount account, boolean staging, JsonObject authorization) throws AcmeException { List<AcmeChallenge> 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; }
String queryAccountStatus(AcmeAccount account, boolean staging) throws AcmeException { Assert.checkNotNullParam("account", account); HttpURLConnection connection = sendPostRequestWithRetries(account, staging, getAccountUrl(account, staging), false, getEncodedJson(EMPTY_PAYLOAD), HttpURLConnection.HTTP_OK); JsonObject jsonResponse = getJsonResponse(connection); return jsonResponse.getString(STATUS); }
created = acmeClient.createAccount(acmeAccount, staging); acmeClient.updateAccount(acmeAccount, staging, acmeAccount.isTermsOfServiceAgreed(), acmeAccount.getContactUrls()); X509CertificateChainAndSigningKey certificateChainAndSigningKey = acmeClient.obtainCertificateChain(acmeAccount, staging, algorithm, keySize, domainNames.toArray(new String[domainNames.size()])); keyStore.setKeyEntry(alias, certificateChainAndSigningKey.getSigningKey(), keyPassword, certificateChainAndSigningKey.getCertificateChain()); ((KeyStoreService) keyStoreService).save();
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); } } }
/** * 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); }
/** * 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()); }
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); } }
Assert.checkNotNullParam("account", account); Assert.checkNotNullParam("domainNames", domainNames); final LinkedHashSet<String> domainNamesSet = getDomainNames(domainNames); final String newOrderUrl = getResourceUrl(account, AcmeResource.NEW_ORDER, staging).toString(); JsonArrayBuilder identifiersBuilder = Json.createArrayBuilder(); for (String domainName : domainNamesSet) { HttpURLConnection connection = sendPostRequestWithRetries(account, staging, newOrderUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_CREATED); JsonObject jsonResponse = getJsonResponse(connection); final String finalizeOrderUrl = jsonResponse.getString(FINALIZE); final JsonArray authorizationsArray = jsonResponse.getJsonArray(AUTHORIZATIONS); try { for (String authorizationUrl : authorizationUrls) { connection = sendGetRequest(authorizationUrl, HttpURLConnection.HTTP_OK, JSON_CONTENT_TYPE); jsonResponse = getJsonResponse(connection); AcmeChallenge selectedChallenge = respondToChallenges(account, staging, jsonResponse); if (selectedChallenge != null) { selectedChallenges.add(selectedChallenge); jsonResponse = pollResourceUntilFinalized(authorizationUrl); if (! jsonResponse.getString(STATUS).equals(VALID)) { throw acme.challengeResponseFailedValidationByAcmeServer(); connection = sendPostRequestWithRetries(account, staging, finalizeOrderUrl, false, getEncodedJson(payloadBuilder.build()), HttpURLConnection.HTTP_OK); final String orderUrl = getLocation(connection); jsonResponse = pollResourceUntilFinalized(orderUrl);
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); JsonObject jsonResponse = getJsonResponse(connection); AcmeChallenge selectedChallenge = respondToChallenges(account, staging, jsonResponse); try { jsonResponse = pollResourceUntilFinalized(authorizationUrl); if (! jsonResponse.getString(STATUS).equals(VALID)) { throw acme.challengeResponseFailedValidationByAcmeServer(); } finally { if (selectedChallenge != null) { cleanupAfterChallenge(account, selectedChallenge);