001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2016 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j;
015
016import static java.util.Objects.requireNonNull;
017import static org.jose4j.jws.AlgorithmIdentifiers.*;
018import static org.shredzone.acme4j.toolbox.JoseUtils.macKeyAlgorithm;
019
020import java.net.URI;
021import java.security.KeyPair;
022import java.util.ArrayList;
023import java.util.List;
024import java.util.Set;
025
026import javax.crypto.SecretKey;
027import javax.crypto.spec.SecretKeySpec;
028
029import edu.umd.cs.findbugs.annotations.Nullable;
030import org.shredzone.acme4j.connector.Resource;
031import org.shredzone.acme4j.exception.AcmeException;
032import org.shredzone.acme4j.toolbox.AcmeUtils;
033import org.shredzone.acme4j.toolbox.JSONBuilder;
034import org.shredzone.acme4j.toolbox.JoseUtils;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * A builder for registering a new account with the CA.
040 * <p>
041 * You need to create a new key pair and set it via {@link #useKeyPair(KeyPair)}. Your
042 * account will be identified by the public part of that key pair, so make sure to store
043 * it safely! There is no automatic way to regain access to your account if the key pair
044 * is lost.
045 * <p>
046 * Depending on the CA you register with, you might need to give additional information.
047 * <ul>
048 *     <li>You might need to agree to the terms of service via
049 *     {@link #agreeToTermsOfService()}.</li>
050 *     <li>You might need to give at least one contact URI.</li>
051 *     <li>You might need to provide a key identifier (e.g. your customer number) and
052 *     a shared secret via {@link #withKeyIdentifier(String, SecretKey)}.</li>
053 * </ul>
054 * <p>
055 * It is not possible to modify an existing account with the {@link AccountBuilder}. To
056 * modify an existing account, use {@link Account#modify()} and
057 * {@link Account#changeKey(KeyPair)}.
058 */
059public class AccountBuilder {
060    private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class);
061    private static final Set<String> VALID_ALGORITHMS = Set.of(HMAC_SHA256, HMAC_SHA384, HMAC_SHA512);
062
063    private final List<URI> contacts = new ArrayList<>();
064    private @Nullable Boolean termsOfServiceAgreed;
065    private @Nullable Boolean onlyExisting;
066    private @Nullable String keyIdentifier;
067    private @Nullable KeyPair keyPair;
068    private @Nullable SecretKey macKey;
069    private @Nullable String macAlgorithm;
070
071    /**
072     * Add a contact URI to the list of contacts.
073     * <p>
074     * A contact URI may be e.g. an email address or a phone number. It depends on the CA
075     * what kind of contact URIs are accepted, and how many must be provided as minimum.
076     *
077     * @param contact
078     *         Contact URI
079     * @return itself
080     */
081    public AccountBuilder addContact(URI contact) {
082        AcmeUtils.validateContact(contact);
083        contacts.add(contact);
084        return this;
085    }
086
087    /**
088     * Add a contact address to the list of contacts.
089     * <p>
090     * This is a convenience call for {@link #addContact(URI)}.
091     *
092     * @param contact
093     *         Contact URI as string
094     * @return itself
095     * @throws IllegalArgumentException
096     *         if there is a syntax error in the URI string
097     */
098    public AccountBuilder addContact(String contact) {
099        addContact(URI.create(contact));
100        return this;
101    }
102
103    /**
104     * Add an email address to the list of contacts.
105     * <p>
106     * This is a convenience call for {@link #addContact(String)} that doesn't require
107     * to prepend the "mailto" scheme to an email address.
108     *
109     * @param email
110     *         Contact email without "mailto" scheme (e.g. test@gmail.com)
111     * @return itself
112     * @throws IllegalArgumentException
113     *         if there is a syntax error in the URI string
114     */
115    public AccountBuilder addEmail(String email) {
116        if (email.startsWith("mailto:")) {
117            addContact(email);
118        } else {
119            addContact("mailto:" + email);
120        }
121        return this;
122    }
123
124    /**
125     * Documents that the user has agreed to the terms of service.
126     * <p>
127     * If the CA requires the user to agree to the terms of service, it is your
128     * responsibility to present them to the user, and actively ask for their agreement. A
129     * link to the terms of service is provided via
130     * {@code session.getMetadata().getTermsOfService()}.
131     *
132     * @return itself
133     */
134    public AccountBuilder agreeToTermsOfService() {
135        this.termsOfServiceAgreed = true;
136        return this;
137    }
138
139    /**
140     * Signals that only an existing account should be returned. The server will not
141     * create a new account if the key is not known.
142     * <p>
143     * If you have lost your account's location URL, but still have your account's key
144     * pair, you can register your account again with the same key, and use
145     * {@link #onlyExisting()} to make sure that your existing account is returned. If
146     * your key is unknown to the server, an error is thrown once the account is to be
147     * created.
148     *
149     * @return itself
150     */
151    public AccountBuilder onlyExisting() {
152        this.onlyExisting = true;
153        return this;
154    }
155
156    /**
157     * Sets the {@link KeyPair} to be used for this account.
158     * <p>
159     * Only the public key of the pair is sent to the server for registration. acme4j will
160     * never send the private key part.
161     * <p>
162     * Make sure to store your key pair safely after registration! There is no automatic
163     * way to regain access to your account if the key pair is lost.
164     *
165     * @param keyPair
166     *         Account's {@link KeyPair}
167     * @return itself
168     */
169    public AccountBuilder useKeyPair(KeyPair keyPair) {
170        this.keyPair = requireNonNull(keyPair, "keyPair");
171        return this;
172    }
173
174    /**
175     * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
176     * an individual account identification (e.g. your customer number) and a shared
177     * secret for registration. See the documentation of your CA about how to retrieve the
178     * key identifier and MAC key.
179     *
180     * @param kid
181     *         Key Identifier
182     * @param macKey
183     *         MAC key
184     * @return itself
185     * @see #withKeyIdentifier(String, String)
186     */
187    public AccountBuilder withKeyIdentifier(String kid, SecretKey macKey) {
188        if (kid != null && kid.isEmpty()) {
189            throw new IllegalArgumentException("kid must not be empty");
190        }
191        this.macKey = requireNonNull(macKey, "macKey");
192        this.keyIdentifier = kid;
193        return this;
194    }
195
196    /**
197     * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
198     * an individual account identification (e.g. your customer number) and a shared
199     * secret for registration. See the documentation of your CA about how to retrieve the
200     * key identifier and MAC key.
201     * <p>
202     * This is a convenience call of {@link #withKeyIdentifier(String, SecretKey)} that
203     * accepts a base64url encoded MAC key, so both parameters can be passed in as
204     * strings.
205     *
206     * @param kid
207     *         Key Identifier
208     * @param encodedMacKey
209     *         Base64url encoded MAC key.
210     * @return itself
211     * @see #withKeyIdentifier(String, SecretKey)
212     */
213    public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) {
214        var encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey"));
215        return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC"));
216    }
217
218    /**
219     * Sets the MAC key algorithm that is provided by the CA. To be used in combination
220     * with key identifier. By default, the algorithm is deduced from the size of the
221     * MAC key. If a different size is needed, it can be set using this method.
222     *
223     * @param macAlgorithm
224     *         the algorithm to be set in the {@code alg} field, e.g. {@code "HS512"}.
225     * @return itself
226     * @since 3.1.0
227     */
228    public AccountBuilder withMacAlgorithm(String macAlgorithm) {
229        var algorithm = requireNonNull(macAlgorithm, "macAlgorithm");
230        if (!VALID_ALGORITHMS.contains(algorithm)) {
231            throw new IllegalArgumentException("Invalid MAC algorithm: " + macAlgorithm);
232        }
233        this.macAlgorithm = algorithm;
234        return this;
235    }
236
237    /**
238     * Creates a new account.
239     * <p>
240     * Use this method to finally create your account with the given parameters. Do not
241     * use the {@link AccountBuilder} after invoking this method.
242     *
243     * @param session
244     *         {@link Session} to be used for registration
245     * @return {@link Account} referring to the new account
246     * @see #createLogin(Session)
247     */
248    public Account create(Session session) throws AcmeException {
249        return createLogin(session).getAccount();
250    }
251
252    /**
253     * Creates a new account.
254     * <p>
255     * This method is identical to {@link #create(Session)}, but returns a {@link Login}
256     * that is ready to be used.
257     *
258     * @param session
259     *         {@link Session} to be used for registration
260     * @return {@link Login} referring to the new account
261     */
262    public Login createLogin(Session session) throws AcmeException {
263        requireNonNull(session, "session");
264
265        if (keyPair == null) {
266            throw new IllegalStateException("Use AccountBuilder.useKeyPair() to set the account's key pair.");
267        }
268
269        LOG.debug("create");
270
271        try (var conn = session.connect()) {
272            var resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT);
273
274            var claims = new JSONBuilder();
275            if (!contacts.isEmpty()) {
276                claims.put("contact", contacts);
277            }
278            if (termsOfServiceAgreed != null) {
279                claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
280            }
281            if (keyIdentifier != null && macKey != null) {
282                var algorithm = macAlgorithm != null ? macAlgorithm : macKeyAlgorithm(macKey);
283                claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
284                        keyIdentifier, keyPair.getPublic(), macKey, algorithm, resourceUrl));
285            }
286            if (onlyExisting != null) {
287                claims.put("onlyReturnExisting", onlyExisting);
288            }
289
290            conn.sendSignedRequest(resourceUrl, claims, session, keyPair);
291
292            var login = new Login(conn.getLocation(), keyPair, session);
293            login.getAccount().setJSON(conn.readJsonResponse());
294            return login;
295        }
296    }
297
298}