001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2015 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 net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; 017import static org.assertj.core.api.Assertions.assertThat; 018import static org.assertj.core.api.Assertions.assertThatException; 019import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; 020import static org.shredzone.acme4j.toolbox.TestUtils.url; 021 022import java.net.HttpURLConnection; 023import java.net.URL; 024import java.security.KeyPair; 025 026import edu.umd.cs.findbugs.annotations.Nullable; 027import org.jose4j.jwx.CompactSerializer; 028import org.junit.jupiter.api.Test; 029import org.junit.jupiter.params.ParameterizedTest; 030import org.junit.jupiter.params.provider.CsvSource; 031import org.junit.jupiter.params.provider.NullAndEmptySource; 032import org.junit.jupiter.params.provider.ValueSource; 033import org.mockito.Mockito; 034import org.shredzone.acme4j.connector.Resource; 035import org.shredzone.acme4j.provider.TestableConnectionProvider; 036import org.shredzone.acme4j.toolbox.AcmeUtils; 037import org.shredzone.acme4j.toolbox.JSON; 038import org.shredzone.acme4j.toolbox.JSONBuilder; 039import org.shredzone.acme4j.toolbox.JoseUtilsTest; 040import org.shredzone.acme4j.toolbox.TestUtils; 041 042/** 043 * Unit tests for {@link AccountBuilder}. 044 */ 045public class AccountBuilderTest { 046 047 private final URL resourceUrl = url("http://example.com/acme/resource"); 048 private final URL locationUrl = url("http://example.com/acme/account"); 049 050 /** 051 * Test if a new account can be created. 052 */ 053 @Test 054 public void testRegistration() throws Exception { 055 var accountKey = TestUtils.createKeyPair(); 056 057 var provider = new TestableConnectionProvider() { 058 private boolean isUpdate; 059 060 @Override 061 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 062 assertThat(login).isNotNull(); 063 assertThat(url).isEqualTo(locationUrl); 064 assertThat(isUpdate).isFalse(); 065 isUpdate = true; 066 return HttpURLConnection.HTTP_OK; 067 } 068 069 @Override 070 public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair) { 071 assertThat(session).isNotNull(); 072 assertThat(url).isEqualTo(resourceUrl); 073 assertThatJson(claims.toString()).isEqualTo(getJSON("newAccount").toString()); 074 assertThat(keypair).isEqualTo(accountKey); 075 isUpdate = false; 076 return HttpURLConnection.HTTP_CREATED; 077 } 078 079 @Override 080 public URL getLocation() { 081 return locationUrl; 082 } 083 084 @Override 085 public JSON readJsonResponse() { 086 return getJSON("newAccountResponse"); 087 } 088 }; 089 090 provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl); 091 092 var builder = new AccountBuilder(); 093 builder.addContact("mailto:foo@example.com"); 094 builder.agreeToTermsOfService(); 095 builder.useKeyPair(accountKey); 096 097 var session = provider.createSession(); 098 var login = builder.createLogin(session); 099 100 assertThat(login.getAccountLocation()).isEqualTo(locationUrl); 101 102 var account = login.getAccount(); 103 assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue(); 104 assertThat(account.getLocation()).isEqualTo(locationUrl); 105 assertThat(account.hasExternalAccountBinding()).isFalse(); 106 assertThat(account.getKeyIdentifier()).isEmpty(); 107 108 provider.close(); 109 } 110 111 /** 112 * Test if a new account with Key Identifier can be created. 113 */ 114 @ParameterizedTest 115 @CsvSource({ 116 "SHA-256,HS256,", "SHA-384,HS384,", "SHA-512,HS512,", 117 "SHA-256,HS256,HS256", "SHA-384,HS384,HS384", "SHA-512,HS512,HS512", 118 "SHA-512,HS256,HS256" 119 }) 120 public void testRegistrationWithKid(String keyAlg, String expectedMacAlg, @Nullable String macAlg) throws Exception { 121 var accountKey = TestUtils.createKeyPair(); 122 var keyIdentifier = "NCC-1701"; 123 var macKey = TestUtils.createSecretKey(keyAlg); 124 125 var provider = new TestableConnectionProvider() { 126 @Override 127 public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair) { 128 assertThat(session).isNotNull(); 129 assertThat(url).isEqualTo(resourceUrl); 130 assertThat(keypair).isEqualTo(accountKey); 131 132 var binding = claims.toJSON() 133 .get("externalAccountBinding") 134 .asObject(); 135 136 var encodedHeader = binding.get("protected").asString(); 137 var encodedSignature = binding.get("signature").asString(); 138 var encodedPayload = binding.get("payload").asString(); 139 var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature); 140 141 JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, expectedMacAlg); 142 143 return HttpURLConnection.HTTP_CREATED; 144 } 145 146 @Override 147 public URL getLocation() { 148 return locationUrl; 149 } 150 151 @Override 152 public JSON readJsonResponse() { 153 return JSON.empty(); 154 } 155 }; 156 157 provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl); 158 provider.putMetadata("externalAccountRequired", true); 159 160 var builder = new AccountBuilder(); 161 builder.useKeyPair(accountKey); 162 builder.withKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded())); 163 if (macAlg != null) { 164 builder.withMacAlgorithm(macAlg); 165 } 166 167 var session = provider.createSession(); 168 var login = builder.createLogin(session); 169 170 assertThat(login.getAccountLocation()).isEqualTo(locationUrl); 171 172 provider.close(); 173 } 174 175 /** 176 * Test if invalid mac algorithms are rejected. 177 */ 178 @ParameterizedTest 179 @NullAndEmptySource 180 @ValueSource(strings = {"foo", "null", "false", "none", "HS-256", "hs256", "HS128", "RS256"}) 181 public void testRejectInvalidMacAlg(@Nullable String macAlg) { 182 assertThatException().isThrownBy(() -> { 183 new AccountBuilder().withMacAlgorithm(macAlg); 184 }).isInstanceOfAny(IllegalArgumentException.class, NullPointerException.class); 185 } 186 187 /** 188 * Test if an existing account is properly returned. 189 */ 190 @Test 191 public void testOnlyExistingRegistration() throws Exception { 192 var accountKey = TestUtils.createKeyPair(); 193 194 var provider = new TestableConnectionProvider() { 195 @Override 196 public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair) { 197 assertThat(session).isNotNull(); 198 assertThat(url).isEqualTo(resourceUrl); 199 assertThatJson(claims.toString()).isEqualTo(getJSON("newAccountOnlyExisting").toString()); 200 assertThat(keypair).isEqualTo(accountKey); 201 return HttpURLConnection.HTTP_OK; 202 } 203 204 @Override 205 public URL getLocation() { 206 return locationUrl; 207 } 208 209 @Override 210 public JSON readJsonResponse() { 211 return getJSON("newAccountResponse"); 212 } 213 }; 214 215 provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl); 216 217 var builder = new AccountBuilder(); 218 builder.useKeyPair(accountKey); 219 builder.onlyExisting(); 220 221 var session = provider.createSession(); 222 var login = builder.createLogin(session); 223 224 assertThat(login.getAccountLocation()).isEqualTo(locationUrl); 225 226 provider.close(); 227 } 228 229 @Test 230 public void testEmailAddresses() { 231 var builder = Mockito.spy(AccountBuilder.class); 232 builder.addEmail("foo@example.com"); 233 Mockito.verify(builder).addContact(Mockito.eq("mailto:foo@example.com")); 234 235 // mailto is still accepted if present 236 builder.addEmail("mailto:bar@example.com"); 237 Mockito.verify(builder).addContact(Mockito.eq("mailto:bar@example.com")); 238 } 239}