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}