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;
017
018import java.net.URI;
019import java.net.URL;
020import java.security.KeyPair;
021import java.util.ArrayList;
022import java.util.List;
023
024import javax.crypto.SecretKey;
025import javax.crypto.spec.SecretKeySpec;
026
027import edu.umd.cs.findbugs.annotations.Nullable;
028import org.shredzone.acme4j.connector.Connection;
029import org.shredzone.acme4j.connector.Resource;
030import org.shredzone.acme4j.exception.AcmeException;
031import org.shredzone.acme4j.exception.AcmeProtocolException;
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.
040 */
041public class AccountBuilder {
042    private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class);
043
044    private final List<URI> contacts = new ArrayList<>();
045    private @Nullable Boolean termsOfServiceAgreed;
046    private @Nullable Boolean onlyExisting;
047    private @Nullable String keyIdentifier;
048    private @Nullable KeyPair keyPair;
049    private @Nullable SecretKey macKey;
050
051    /**
052     * Add a contact URI to the list of contacts.
053     *
054     * @param contact
055     *            Contact URI
056     * @return itself
057     */
058    public AccountBuilder addContact(URI contact) {
059        AcmeUtils.validateContact(contact);
060        contacts.add(contact);
061        return this;
062    }
063
064    /**
065     * Add a contact address to the list of contacts.
066     * <p>
067     * This is a convenience call for {@link #addContact(URI)}.
068     *
069     * @param contact
070     *            Contact URI as string
071     * @throws IllegalArgumentException
072     *             if there is a syntax error in the URI string
073     * @return itself
074     */
075    public AccountBuilder addContact(String contact) {
076        addContact(URI.create(contact));
077        return this;
078    }
079
080    /**
081     * Add a email address to the list of contacts.
082     * <p>
083     * This is a convenience call for {@link #addContact(String)} that doesn't
084     * require from you attach "mailto" scheme before email address.
085     *
086     * @param email
087     *             Contact email without "mailto" scheme (e.g. test@gmail.com)
088     * @throws IllegalArgumentException
089     *             if there is a syntax error in the URI string
090     * @return itself
091     */
092    public AccountBuilder addEmail(String email) {
093        addContact("mailto:" + email);
094        return this;
095    }
096
097    /**
098     * Signals that the user agrees to the terms of service.
099     *
100     * @return itself
101     */
102    public AccountBuilder agreeToTermsOfService() {
103        this.termsOfServiceAgreed = true;
104        return this;
105    }
106
107    /**
108     * Signals that only an existing account should be returned. The server will not
109     * create a new account if the key is not known. This is useful if you only have your
110     * account's key pair available, but not your account's location URL.
111     *
112     * @return itself
113     */
114    public AccountBuilder onlyExisting() {
115        this.onlyExisting = true;
116        return this;
117    }
118
119    /**
120     * Sets the {@link KeyPair} to be used for this account.
121     *
122     * @param keyPair
123     *            Account's {@link KeyPair}
124     * @return itself
125     */
126    public AccountBuilder useKeyPair(KeyPair keyPair) {
127        this.keyPair = requireNonNull(keyPair, "keyPair");
128        return this;
129    }
130
131    /**
132     * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
133     * an individual account identification, e.g. your customer number.
134     *
135     * @param kid
136     *            Key Identifier
137     * @param macKey
138     *            MAC key
139     * @return itself
140     */
141    public AccountBuilder withKeyIdentifier(String kid, SecretKey macKey) {
142        if (kid != null && kid.isEmpty()) {
143            throw new IllegalArgumentException("kid must not be empty");
144        }
145        this.macKey = requireNonNull(macKey, "macKey");
146        this.keyIdentifier = kid;
147        return this;
148    }
149
150    /**
151     * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
152     * an individual account identification, e.g. your customer number.
153     *
154     * @param kid
155     *            Key Identifier
156     * @param encodedMacKey
157     *            Base64url encoded MAC key. It will be decoded for your convenience.
158     * @return itself
159     */
160    public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) {
161        byte[] encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey"));
162        return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC"));
163    }
164
165    /**
166     * Creates a new account.
167     *
168     * @param session
169     *            {@link Session} to be used for registration
170     * @return {@link Account} referring to the new account
171     */
172    public Account create(Session session) throws AcmeException {
173        return createLogin(session).getAccount();
174    }
175
176    /**
177     * Creates a new account.
178     * <p>
179     * This method returns a ready to use {@link Login} for the new {@link Account}.
180     *
181     * @param session
182     *            {@link Session} to be used for registration
183     * @return {@link Login} referring to the new account
184     */
185    public Login createLogin(Session session) throws AcmeException {
186        requireNonNull(session, "session");
187
188        if (keyPair == null) {
189            throw new IllegalStateException("Use AccountBuilder.useKeyPair() to set the account's key pair.");
190        }
191
192        LOG.debug("create");
193
194        try (Connection conn = session.connect()) {
195            URL resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT);
196
197            JSONBuilder claims = new JSONBuilder();
198            if (!contacts.isEmpty()) {
199                claims.put("contact", contacts);
200            }
201            if (termsOfServiceAgreed != null) {
202                claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
203            }
204            if (keyIdentifier != null) {
205                claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
206                        keyIdentifier, keyPair.getPublic(), macKey, resourceUrl));
207            }
208            if (onlyExisting != null) {
209                claims.put("onlyReturnExisting", onlyExisting);
210            }
211
212            conn.sendSignedRequest(resourceUrl, claims, session, keyPair);
213
214            URL location = conn.getLocation();
215            if (location == null) {
216                throw new AcmeProtocolException("Server did not provide an account location");
217            }
218
219            Login login = new Login(location, keyPair, session);
220            login.getAccount().setJSON(conn.readJsonResponse());
221            return login;
222        }
223    }
224
225}