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