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; 017import static org.jose4j.jws.AlgorithmIdentifiers.*; 018import static org.shredzone.acme4j.toolbox.JoseUtils.macKeyAlgorithm; 019 020import java.net.URI; 021import java.security.KeyPair; 022import java.util.ArrayList; 023import java.util.List; 024import java.util.Set; 025 026import javax.crypto.SecretKey; 027import javax.crypto.spec.SecretKeySpec; 028 029import edu.umd.cs.findbugs.annotations.Nullable; 030import org.shredzone.acme4j.connector.Resource; 031import org.shredzone.acme4j.exception.AcmeException; 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 with the CA. 040 * <p> 041 * You need to create a new key pair and set it via {@link #useKeyPair(KeyPair)}. Your 042 * account will be identified by the public part of that key pair, so make sure to store 043 * it safely! There is no automatic way to regain access to your account if the key pair 044 * is lost. 045 * <p> 046 * Depending on the CA you register with, you might need to give additional information. 047 * <ul> 048 * <li>You might need to agree to the terms of service via 049 * {@link #agreeToTermsOfService()}.</li> 050 * <li>You might need to give at least one contact URI.</li> 051 * <li>You might need to provide a key identifier (e.g. your customer number) and 052 * a shared secret via {@link #withKeyIdentifier(String, SecretKey)}.</li> 053 * </ul> 054 * <p> 055 * It is not possible to modify an existing account with the {@link AccountBuilder}. To 056 * modify an existing account, use {@link Account#modify()} and 057 * {@link Account#changeKey(KeyPair)}. 058 */ 059public class AccountBuilder { 060 private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class); 061 private static final Set<String> VALID_ALGORITHMS = Set.of(HMAC_SHA256, HMAC_SHA384, HMAC_SHA512); 062 063 private final List<URI> contacts = new ArrayList<>(); 064 private @Nullable Boolean termsOfServiceAgreed; 065 private @Nullable Boolean onlyExisting; 066 private @Nullable String keyIdentifier; 067 private @Nullable KeyPair keyPair; 068 private @Nullable SecretKey macKey; 069 private @Nullable String macAlgorithm; 070 071 /** 072 * Add a contact URI to the list of contacts. 073 * <p> 074 * A contact URI may be e.g. an email address or a phone number. It depends on the CA 075 * what kind of contact URIs are accepted, and how many must be provided as minimum. 076 * 077 * @param contact 078 * Contact URI 079 * @return itself 080 */ 081 public AccountBuilder addContact(URI contact) { 082 AcmeUtils.validateContact(contact); 083 contacts.add(contact); 084 return this; 085 } 086 087 /** 088 * Add a contact address to the list of contacts. 089 * <p> 090 * This is a convenience call for {@link #addContact(URI)}. 091 * 092 * @param contact 093 * Contact URI as string 094 * @return itself 095 * @throws IllegalArgumentException 096 * if there is a syntax error in the URI string 097 */ 098 public AccountBuilder addContact(String contact) { 099 addContact(URI.create(contact)); 100 return this; 101 } 102 103 /** 104 * Add an email address to the list of contacts. 105 * <p> 106 * This is a convenience call for {@link #addContact(String)} that doesn't require 107 * to prepend the "mailto" scheme to an email address. 108 * 109 * @param email 110 * Contact email without "mailto" scheme (e.g. test@gmail.com) 111 * @return itself 112 * @throws IllegalArgumentException 113 * if there is a syntax error in the URI string 114 */ 115 public AccountBuilder addEmail(String email) { 116 if (email.startsWith("mailto:")) { 117 addContact(email); 118 } else { 119 addContact("mailto:" + email); 120 } 121 return this; 122 } 123 124 /** 125 * Documents that the user has agreed to the terms of service. 126 * <p> 127 * If the CA requires the user to agree to the terms of service, it is your 128 * responsibility to present them to the user, and actively ask for their agreement. A 129 * link to the terms of service is provided via 130 * {@code session.getMetadata().getTermsOfService()}. 131 * 132 * @return itself 133 */ 134 public AccountBuilder agreeToTermsOfService() { 135 this.termsOfServiceAgreed = true; 136 return this; 137 } 138 139 /** 140 * Signals that only an existing account should be returned. The server will not 141 * create a new account if the key is not known. 142 * <p> 143 * If you have lost your account's location URL, but still have your account's key 144 * pair, you can register your account again with the same key, and use 145 * {@link #onlyExisting()} to make sure that your existing account is returned. If 146 * your key is unknown to the server, an error is thrown once the account is to be 147 * created. 148 * 149 * @return itself 150 */ 151 public AccountBuilder onlyExisting() { 152 this.onlyExisting = true; 153 return this; 154 } 155 156 /** 157 * Sets the {@link KeyPair} to be used for this account. 158 * <p> 159 * Only the public key of the pair is sent to the server for registration. acme4j will 160 * never send the private key part. 161 * <p> 162 * Make sure to store your key pair safely after registration! There is no automatic 163 * way to regain access to your account if the key pair is lost. 164 * 165 * @param keyPair 166 * Account's {@link KeyPair} 167 * @return itself 168 */ 169 public AccountBuilder useKeyPair(KeyPair keyPair) { 170 this.keyPair = requireNonNull(keyPair, "keyPair"); 171 return this; 172 } 173 174 /** 175 * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires 176 * an individual account identification (e.g. your customer number) and a shared 177 * secret for registration. See the documentation of your CA about how to retrieve the 178 * key identifier and MAC key. 179 * 180 * @param kid 181 * Key Identifier 182 * @param macKey 183 * MAC key 184 * @return itself 185 * @see #withKeyIdentifier(String, String) 186 */ 187 public AccountBuilder withKeyIdentifier(String kid, SecretKey macKey) { 188 if (kid != null && kid.isEmpty()) { 189 throw new IllegalArgumentException("kid must not be empty"); 190 } 191 this.macKey = requireNonNull(macKey, "macKey"); 192 this.keyIdentifier = kid; 193 return this; 194 } 195 196 /** 197 * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires 198 * an individual account identification (e.g. your customer number) and a shared 199 * secret for registration. See the documentation of your CA about how to retrieve the 200 * key identifier and MAC key. 201 * <p> 202 * This is a convenience call of {@link #withKeyIdentifier(String, SecretKey)} that 203 * accepts a base64url encoded MAC key, so both parameters can be passed in as 204 * strings. 205 * 206 * @param kid 207 * Key Identifier 208 * @param encodedMacKey 209 * Base64url encoded MAC key. 210 * @return itself 211 * @see #withKeyIdentifier(String, SecretKey) 212 */ 213 public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) { 214 var encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey")); 215 return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC")); 216 } 217 218 /** 219 * Sets the MAC key algorithm that is provided by the CA. To be used in combination 220 * with key identifier. By default, the algorithm is deduced from the size of the 221 * MAC key. If a different size is needed, it can be set using this method. 222 * 223 * @param macAlgorithm 224 * the algorithm to be set in the {@code alg} field, e.g. {@code "HS512"}. 225 * @return itself 226 * @since 3.1.0 227 */ 228 public AccountBuilder withMacAlgorithm(String macAlgorithm) { 229 var algorithm = requireNonNull(macAlgorithm, "macAlgorithm"); 230 if (!VALID_ALGORITHMS.contains(algorithm)) { 231 throw new IllegalArgumentException("Invalid MAC algorithm: " + macAlgorithm); 232 } 233 this.macAlgorithm = algorithm; 234 return this; 235 } 236 237 /** 238 * Creates a new account. 239 * <p> 240 * Use this method to finally create your account with the given parameters. Do not 241 * use the {@link AccountBuilder} after invoking this method. 242 * 243 * @param session 244 * {@link Session} to be used for registration 245 * @return {@link Account} referring to the new account 246 * @see #createLogin(Session) 247 */ 248 public Account create(Session session) throws AcmeException { 249 return createLogin(session).getAccount(); 250 } 251 252 /** 253 * Creates a new account. 254 * <p> 255 * This method is identical to {@link #create(Session)}, but returns a {@link Login} 256 * that is ready to be used. 257 * 258 * @param session 259 * {@link Session} to be used for registration 260 * @return {@link Login} referring to the new account 261 */ 262 public Login createLogin(Session session) throws AcmeException { 263 requireNonNull(session, "session"); 264 265 if (keyPair == null) { 266 throw new IllegalStateException("Use AccountBuilder.useKeyPair() to set the account's key pair."); 267 } 268 269 LOG.debug("create"); 270 271 try (var conn = session.connect()) { 272 var resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT); 273 274 var claims = new JSONBuilder(); 275 if (!contacts.isEmpty()) { 276 claims.put("contact", contacts); 277 } 278 if (termsOfServiceAgreed != null) { 279 claims.put("termsOfServiceAgreed", termsOfServiceAgreed); 280 } 281 if (keyIdentifier != null && macKey != null) { 282 var algorithm = macAlgorithm != null ? macAlgorithm : macKeyAlgorithm(macKey); 283 claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding( 284 keyIdentifier, keyPair.getPublic(), macKey, algorithm, resourceUrl)); 285 } 286 if (onlyExisting != null) { 287 claims.put("onlyReturnExisting", onlyExisting); 288 } 289 290 conn.sendSignedRequest(resourceUrl, claims, session, keyPair); 291 292 var login = new Login(conn.getLocation(), keyPair, session); 293 login.getAccount().setJSON(conn.readJsonResponse()); 294 return login; 295 } 296 } 297 298}