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