001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2018 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.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;
018
019import java.net.MalformedURLException;
020import java.net.URI;
021import java.net.URL;
022import java.security.KeyPair;
023import java.security.PublicKey;
024import java.security.cert.X509Certificate;
025
026import edu.umd.cs.findbugs.annotations.Nullable;
027import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
028import org.shredzone.acme4j.challenge.Challenge;
029import org.shredzone.acme4j.connector.Resource;
030import org.shredzone.acme4j.exception.AcmeException;
031import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
032import org.shredzone.acme4j.exception.AcmeProtocolException;
033import org.shredzone.acme4j.toolbox.JSON;
034import org.shredzone.acme4j.toolbox.JSONBuilder;
035import org.shredzone.acme4j.toolbox.JoseUtils;
036
037/**
038 * A {@link Login} into an account.
039 * <p>
040 * A login is bound to a {@link Session}. However, a {@link Session} can handle multiple
041 * logins in parallel.
042 * <p>
043 * To create a login, you need to specify the location URI of the {@link Account}, and
044 * need to provide the {@link KeyPair} the account was created with. If the account's
045 * location URL is unknown, the account can be re-registered with the
046 * {@link AccountBuilder}, using {@link AccountBuilder#onlyExisting()} to make sure that
047 * no new account will be created. If the key pair was lost though, there is no automatic
048 * way to regain access to your account, and you have to contact your CA's support hotline
049 * for assistance.
050 * <p>
051 * Note that {@link Login} objects are intentionally not serializable, as they contain a
052 * keypair and volatile data. On distributed systems, you can create a {@link Login} to
053 * the same account for every service instance.
054 */
055public class Login {
056
057    private final Session session;
058    private final Account account;
059    private KeyPair keyPair;
060
061    /**
062     * Creates a new {@link Login}.
063     *
064     * @param accountLocation
065     *            Account location {@link URL}
066     * @param keyPair
067     *            {@link KeyPair} of the account
068     * @param session
069     *            {@link Session} to be used
070     */
071    public Login(URL accountLocation, KeyPair keyPair, Session session) {
072        this.keyPair = requireNonNull(keyPair, "keyPair");
073        this.session = requireNonNull(session, "session");
074        this.account = new Account(this, requireNonNull(accountLocation, "accountLocation"));
075    }
076
077    /**
078     * Gets the {@link Session} that is used.
079     */
080    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
081    public Session getSession() {
082        return session;
083    }
084
085    /**
086     * Gets the {@link PublicKey} of the ACME account.
087     *
088     * @since 5.0.0
089     */
090    public PublicKey getPublicKey() {
091        return keyPair.getPublic();
092    }
093
094    /**
095     * Gets the {@link Account} that is bound to this login.
096     *
097     * @return {@link Account} bound to the login
098     */
099    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
100    public Account getAccount() {
101        return account;
102    }
103
104    /**
105     * Creates a new instance of an existing {@link Authorization} and binds it to this
106     * login.
107     *
108     * @param location
109     *         Location of the Authorization
110     * @return {@link Authorization} bound to the login
111     */
112    public Authorization bindAuthorization(URL location) {
113        return new Authorization(this, requireNonNull(location, "location"));
114    }
115
116    /**
117     * Creates a new instance of an existing {@link Certificate} and binds it to this
118     * login.
119     *
120     * @param location
121     *         Location of the Certificate
122     * @return {@link Certificate} bound to the login
123     */
124    public Certificate bindCertificate(URL location) {
125        return new Certificate(this, requireNonNull(location, "location"));
126    }
127
128    /**
129     * Creates a new instance of an existing {@link Order} and binds it to this login.
130     *
131     * @param location
132     *         Location URL of the order
133     * @return {@link Order} bound to the login
134     */
135    public Order bindOrder(URL location) {
136        return new Order(this, requireNonNull(location, "location"));
137    }
138
139    /**
140     * Creates a new instance of an existing {@link RenewalInfo} and binds it to this
141     * login.
142     *
143     * @param location
144     *         Location URL of the renewal info
145     * @return {@link RenewalInfo} bound to the login
146     * @since 3.0.0
147     */
148    public RenewalInfo bindRenewalInfo(URL location) {
149        return new RenewalInfo(this, requireNonNull(location, "location"));
150    }
151
152    /**
153     * Creates a new instance of an existing {@link RenewalInfo} and binds it to this
154     * login.
155     *
156     * @param certificate
157     *         {@link X509Certificate} to get the {@link RenewalInfo} for
158     * @return {@link RenewalInfo} bound to the login
159     * @since 3.2.0
160     */
161    public RenewalInfo bindRenewalInfo(X509Certificate certificate) throws AcmeException {
162        try {
163            var url = getSession().resourceUrl(Resource.RENEWAL_INFO).toExternalForm();
164            if (!url.endsWith("/")) {
165                url += '/';
166            }
167            url += getRenewalUniqueIdentifier(certificate);
168            return bindRenewalInfo(URI.create(url).toURL());
169        } catch (MalformedURLException ex) {
170            throw new AcmeProtocolException("Invalid RenewalInfo URL", ex);
171        }
172    }
173
174    /**
175     * Creates a new instance of an existing {@link Challenge} and binds it to this
176     * login. Use this method only if the resulting challenge type is unknown.
177     *
178     * @param location
179     *         Location URL of the challenge
180     * @return {@link Challenge} bound to the login
181     * @since 2.8
182     * @see #bindChallenge(URL, Class)
183     */
184    public Challenge bindChallenge(URL location) {
185        try (var connect = session.connect()) {
186            connect.sendSignedPostAsGetRequest(location, this);
187            return createChallenge(connect.readJsonResponse());
188        } catch (AcmeException ex) {
189            throw new AcmeLazyLoadingException(Challenge.class, location, ex);
190        }
191    }
192
193    /**
194     * Creates a new instance of an existing {@link Challenge} and binds it to this
195     * login. Use this method if the resulting challenge type is known.
196     *
197     * @param location
198     *         Location URL of the challenge
199     * @param type
200     *         Expected challenge type
201     * @return Challenge bound to the login
202     * @throws AcmeProtocolException
203     *         if the challenge found at the location does not match the expected
204     *         challenge type.
205     * @since 2.12
206     */
207    public <C extends Challenge> C bindChallenge(URL location, Class<C> type) {
208        var challenge = bindChallenge(location);
209        if (!type.isInstance(challenge)) {
210            throw new AcmeProtocolException("Challenge type " + challenge.getType()
211                    + " does not match requested class " + type);
212        }
213        return type.cast(challenge);
214    }
215
216    /**
217     * Creates a {@link Challenge} instance for the given challenge data.
218     *
219     * @param data
220     *            Challenge JSON data
221     * @return {@link Challenge} instance
222     */
223    public Challenge createChallenge(JSON data) {
224        var challenge = session.provider().createChallenge(this, data);
225        if (challenge == null) {
226            throw new AcmeProtocolException("Could not create challenge for: " + data);
227        }
228        return challenge;
229    }
230
231    /**
232     * Creates a builder for a new {@link Order}.
233     *
234     * @return {@link OrderBuilder} object
235     * @since 3.0.0
236     */
237    public OrderBuilder newOrder() {
238        return new OrderBuilder(this);
239    }
240
241    /**
242     * Sets a different {@link KeyPair}. The new key pair is only used locally in this
243     * instance, but is not set on server side!
244     */
245    protected void setKeyPair(KeyPair keyPair) {
246        this.keyPair = requireNonNull(keyPair, "keyPair");
247    }
248
249    /**
250     * Creates an ACME JOSE request. This method is meant for internal purposes only.
251     *
252     * @param url
253     *         {@link URL} of the ACME call
254     * @param payload
255     *         ACME JSON payload. If {@code null}, a POST-as-GET request is generated
256     *         instead.
257     * @param nonce
258     *         Nonce to be used. {@code null} if no nonce is to be used in the JOSE
259     *         header.
260     * @return JSON structure of the JOSE request, ready to be sent.
261     * @since 5.0.0
262     */
263    public JSONBuilder createJoseRequest(URL url, @Nullable JSONBuilder payload, @Nullable String nonce) {
264        return JoseUtils.createJoseRequest(url, keyPair, payload, nonce, getAccount().getLocation().toString());
265    }
266
267}