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