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; 017 018import java.net.URI; 019import java.net.URL; 020import java.security.KeyPair; 021import java.util.ArrayList; 022import java.util.List; 023 024import javax.crypto.SecretKey; 025import javax.crypto.spec.SecretKeySpec; 026 027import edu.umd.cs.findbugs.annotations.Nullable; 028import org.shredzone.acme4j.connector.Connection; 029import org.shredzone.acme4j.connector.Resource; 030import org.shredzone.acme4j.exception.AcmeException; 031import org.shredzone.acme4j.exception.AcmeProtocolException; 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. 040 */ 041public class AccountBuilder { 042 private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class); 043 044 private final List<URI> contacts = new ArrayList<>(); 045 private @Nullable Boolean termsOfServiceAgreed; 046 private @Nullable Boolean onlyExisting; 047 private @Nullable String keyIdentifier; 048 private @Nullable KeyPair keyPair; 049 private @Nullable SecretKey macKey; 050 051 /** 052 * Add a contact URI to the list of contacts. 053 * 054 * @param contact 055 * Contact URI 056 * @return itself 057 */ 058 public AccountBuilder addContact(URI contact) { 059 AcmeUtils.validateContact(contact); 060 contacts.add(contact); 061 return this; 062 } 063 064 /** 065 * Add a contact address to the list of contacts. 066 * <p> 067 * This is a convenience call for {@link #addContact(URI)}. 068 * 069 * @param contact 070 * Contact URI as string 071 * @throws IllegalArgumentException 072 * if there is a syntax error in the URI string 073 * @return itself 074 */ 075 public AccountBuilder addContact(String contact) { 076 addContact(URI.create(contact)); 077 return this; 078 } 079 080 /** 081 * Add a email address to the list of contacts. 082 * <p> 083 * This is a convenience call for {@link #addContact(String)} that doesn't 084 * require from you attach "mailto" scheme before email address. 085 * 086 * @param email 087 * Contact email without "mailto" scheme (e.g. test@gmail.com) 088 * @throws IllegalArgumentException 089 * if there is a syntax error in the URI string 090 * @return itself 091 */ 092 public AccountBuilder addEmail(String email) { 093 addContact("mailto:" + email); 094 return this; 095 } 096 097 /** 098 * Signals that the user agrees to the terms of service. 099 * 100 * @return itself 101 */ 102 public AccountBuilder agreeToTermsOfService() { 103 this.termsOfServiceAgreed = true; 104 return this; 105 } 106 107 /** 108 * Signals that only an existing account should be returned. The server will not 109 * create a new account if the key is not known. This is useful if you only have your 110 * account's key pair available, but not your account's location URL. 111 * 112 * @return itself 113 */ 114 public AccountBuilder onlyExisting() { 115 this.onlyExisting = true; 116 return this; 117 } 118 119 /** 120 * Sets the {@link KeyPair} to be used for this account. 121 * 122 * @param keyPair 123 * Account's {@link KeyPair} 124 * @return itself 125 */ 126 public AccountBuilder useKeyPair(KeyPair keyPair) { 127 this.keyPair = requireNonNull(keyPair, "keyPair"); 128 return this; 129 } 130 131 /** 132 * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires 133 * an individual account identification, e.g. your customer number. 134 * 135 * @param kid 136 * Key Identifier 137 * @param macKey 138 * MAC key 139 * @return itself 140 */ 141 public AccountBuilder withKeyIdentifier(String kid, SecretKey macKey) { 142 if (kid != null && kid.isEmpty()) { 143 throw new IllegalArgumentException("kid must not be empty"); 144 } 145 this.macKey = requireNonNull(macKey, "macKey"); 146 this.keyIdentifier = kid; 147 return this; 148 } 149 150 /** 151 * Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires 152 * an individual account identification, e.g. your customer number. 153 * 154 * @param kid 155 * Key Identifier 156 * @param encodedMacKey 157 * Base64url encoded MAC key. It will be decoded for your convenience. 158 * @return itself 159 */ 160 public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) { 161 byte[] encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey")); 162 return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC")); 163 } 164 165 /** 166 * Creates a new account. 167 * 168 * @param session 169 * {@link Session} to be used for registration 170 * @return {@link Account} referring to the new account 171 */ 172 public Account create(Session session) throws AcmeException { 173 return createLogin(session).getAccount(); 174 } 175 176 /** 177 * Creates a new account. 178 * <p> 179 * This method returns a ready to use {@link Login} for the new {@link Account}. 180 * 181 * @param session 182 * {@link Session} to be used for registration 183 * @return {@link Login} referring to the new account 184 */ 185 public Login createLogin(Session session) throws AcmeException { 186 requireNonNull(session, "session"); 187 188 if (keyPair == null) { 189 throw new IllegalStateException("Use AccountBuilder.useKeyPair() to set the account's key pair."); 190 } 191 192 LOG.debug("create"); 193 194 try (Connection conn = session.connect()) { 195 URL resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT); 196 197 JSONBuilder claims = new JSONBuilder(); 198 if (!contacts.isEmpty()) { 199 claims.put("contact", contacts); 200 } 201 if (termsOfServiceAgreed != null) { 202 claims.put("termsOfServiceAgreed", termsOfServiceAgreed); 203 } 204 if (keyIdentifier != null) { 205 claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding( 206 keyIdentifier, keyPair.getPublic(), macKey, resourceUrl)); 207 } 208 if (onlyExisting != null) { 209 claims.put("onlyReturnExisting", onlyExisting); 210 } 211 212 conn.sendSignedRequest(resourceUrl, claims, session, keyPair); 213 214 URL location = conn.getLocation(); 215 if (location == null) { 216 throw new AcmeProtocolException("Server did not provide an account location"); 217 } 218 219 Login login = new Login(location, keyPair, session); 220 login.getAccount().setJSON(conn.readJsonResponse()); 221 return login; 222 } 223 } 224 225}