org.shredzone.acme4j.AccountBuilder Maven / Gradle / Ivy
/*
* acme4j - Java ACME client
*
* Copyright (C) 2016 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j;
import static java.util.Objects.requireNonNull;
import static org.jose4j.jws.AlgorithmIdentifiers.*;
import static org.shredzone.acme4j.toolbox.JoseUtils.macKeyAlgorithm;
import java.net.URI;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import edu.umd.cs.findbugs.annotations.Nullable;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.shredzone.acme4j.toolbox.JoseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A builder for registering a new account with the CA.
*
* You need to create a new key pair and set it via {@link #useKeyPair(KeyPair)}. Your
* account will be identified by the public part of that key pair, so make sure to store
* it safely! There is no automatic way to regain access to your account if the key pair
* is lost.
*
* Depending on the CA you register with, you might need to give additional information.
*
* - You might need to agree to the terms of service via
* {@link #agreeToTermsOfService()}.
* - You might need to give at least one contact URI.
* - You might need to provide a key identifier (e.g. your customer number) and
* a shared secret via {@link #withKeyIdentifier(String, SecretKey)}.
*
*
* It is not possible to modify an existing account with the {@link AccountBuilder}. To
* modify an existing account, use {@link Account#modify()} and
* {@link Account#changeKey(KeyPair)}.
*/
public class AccountBuilder {
private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class);
private static final Set VALID_ALGORITHMS = Set.of(HMAC_SHA256, HMAC_SHA384, HMAC_SHA512);
private final List contacts = new ArrayList<>();
private @Nullable Boolean termsOfServiceAgreed;
private @Nullable Boolean onlyExisting;
private @Nullable String keyIdentifier;
private @Nullable KeyPair keyPair;
private @Nullable SecretKey macKey;
private @Nullable String macAlgorithm;
/**
* Add a contact URI to the list of contacts.
*
* A contact URI may be e.g. an email address or a phone number. It depends on the CA
* what kind of contact URIs are accepted, and how many must be provided as minimum.
*
* @param contact
* Contact URI
* @return itself
*/
public AccountBuilder addContact(URI contact) {
AcmeUtils.validateContact(contact);
contacts.add(contact);
return this;
}
/**
* Add a contact address to the list of contacts.
*
* This is a convenience call for {@link #addContact(URI)}.
*
* @param contact
* Contact URI as string
* @return itself
* @throws IllegalArgumentException
* if there is a syntax error in the URI string
*/
public AccountBuilder addContact(String contact) {
addContact(URI.create(contact));
return this;
}
/**
* Add an email address to the list of contacts.
*
* This is a convenience call for {@link #addContact(String)} that doesn't require
* to prepend the "mailto" scheme to an email address.
*
* @param email
* Contact email without "mailto" scheme (e.g. [email protected])
* @return itself
* @throws IllegalArgumentException
* if there is a syntax error in the URI string
*/
public AccountBuilder addEmail(String email) {
if (email.startsWith("mailto:")) {
addContact(email);
} else {
addContact("mailto:" + email);
}
return this;
}
/**
* Documents that the user has agreed to the terms of service.
*
* If the CA requires the user to agree to the terms of service, it is your
* responsibility to present them to the user, and actively ask for their agreement. A
* link to the terms of service is provided via
* {@code session.getMetadata().getTermsOfService()}.
*
* @return itself
*/
public AccountBuilder agreeToTermsOfService() {
this.termsOfServiceAgreed = true;
return this;
}
/**
* Signals that only an existing account should be returned. The server will not
* create a new account if the key is not known.
*
* If you have lost your account's location URL, but still have your account's key
* pair, you can register your account again with the same key, and use
* {@link #onlyExisting()} to make sure that your existing account is returned. If
* your key is unknown to the server, an error is thrown once the account is to be
* created.
*
* @return itself
*/
public AccountBuilder onlyExisting() {
this.onlyExisting = true;
return this;
}
/**
* Sets the {@link KeyPair} to be used for this account.
*
* Only the public key of the pair is sent to the server for registration. acme4j will
* never send the private key part.
*
* Make sure to store your key pair safely after registration! There is no automatic
* way to regain access to your account if the key pair is lost.
*
* @param keyPair
* Account's {@link KeyPair}
* @return itself
*/
public AccountBuilder useKeyPair(KeyPair keyPair) {
this.keyPair = requireNonNull(keyPair, "keyPair");
return this;
}
/**
* Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
* an individual account identification (e.g. your customer number) and a shared
* secret for registration. See the documentation of your CA about how to retrieve the
* key identifier and MAC key.
*
* @param kid
* Key Identifier
* @param macKey
* MAC key
* @return itself
* @see #withKeyIdentifier(String, String)
*/
public AccountBuilder withKeyIdentifier(String kid, SecretKey macKey) {
if (kid != null && kid.isEmpty()) {
throw new IllegalArgumentException("kid must not be empty");
}
this.macKey = requireNonNull(macKey, "macKey");
this.keyIdentifier = kid;
return this;
}
/**
* Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
* an individual account identification (e.g. your customer number) and a shared
* secret for registration. See the documentation of your CA about how to retrieve the
* key identifier and MAC key.
*
* This is a convenience call of {@link #withKeyIdentifier(String, SecretKey)} that
* accepts a base64url encoded MAC key, so both parameters can be passed in as
* strings.
*
* @param kid
* Key Identifier
* @param encodedMacKey
* Base64url encoded MAC key.
* @return itself
* @see #withKeyIdentifier(String, SecretKey)
*/
public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) {
var encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey"));
return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC"));
}
/**
* Sets the MAC key algorithm that is provided by the CA. To be used in combination
* with key identifier. By default, the algorithm is deduced from the size of the
* MAC key. If a different size is needed, it can be set using this method.
*
* @param macAlgorithm
* the algorithm to be set in the {@code alg} field, e.g. {@code "HS512"}.
* @return itself
* @since 3.1.0
*/
public AccountBuilder withMacAlgorithm(String macAlgorithm) {
var algorithm = requireNonNull(macAlgorithm, "macAlgorithm");
if (!VALID_ALGORITHMS.contains(algorithm)) {
throw new IllegalArgumentException("Invalid MAC algorithm: " + macAlgorithm);
}
this.macAlgorithm = algorithm;
return this;
}
/**
* Creates a new account.
*
* Use this method to finally create your account with the given parameters. Do not
* use the {@link AccountBuilder} after invoking this method.
*
* @param session
* {@link Session} to be used for registration
* @return {@link Account} referring to the new account
* @see #createLogin(Session)
*/
public Account create(Session session) throws AcmeException {
return createLogin(session).getAccount();
}
/**
* Creates a new account.
*
* This method is identical to {@link #create(Session)}, but returns a {@link Login}
* that is ready to be used.
*
* @param session
* {@link Session} to be used for registration
* @return {@link Login} referring to the new account
*/
public Login createLogin(Session session) throws AcmeException {
requireNonNull(session, "session");
if (keyPair == null) {
throw new IllegalStateException("Use AccountBuilder.useKeyPair() to set the account's key pair.");
}
LOG.debug("create");
try (var conn = session.connect()) {
var resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT);
var claims = new JSONBuilder();
if (!contacts.isEmpty()) {
claims.put("contact", contacts);
}
if (termsOfServiceAgreed != null) {
claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
}
if (keyIdentifier != null && macKey != null) {
var algorithm = macAlgorithm != null ? macAlgorithm : macKeyAlgorithm(macKey);
claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
keyIdentifier, keyPair.getPublic(), macKey, algorithm, resourceUrl));
}
if (onlyExisting != null) {
claims.put("onlyReturnExisting", onlyExisting);
}
conn.sendSignedRequest(resourceUrl, claims, session, keyPair);
var login = new Login(conn.getLocation(), keyPair, session);
login.getAccount().setJSON(conn.readJsonResponse());
return login;
}
}
}