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 java.util.Collections.emptyList; 017import static java.util.Collections.singletonList; 018import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; 019import static org.assertj.core.api.Assertions.*; 020import static org.junit.jupiter.api.Assertions.assertThrows; 021import static org.junit.jupiter.api.Assertions.fail; 022import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; 023import static org.shredzone.acme4j.toolbox.TestUtils.url; 024 025import java.io.IOException; 026import java.net.HttpURLConnection; 027import java.net.URI; 028import java.net.URL; 029import java.util.Collection; 030import java.util.concurrent.atomic.AtomicBoolean; 031 032import org.jose4j.jws.JsonWebSignature; 033import org.jose4j.jwx.CompactSerializer; 034import org.jose4j.lang.JoseException; 035import org.junit.jupiter.api.Test; 036import org.shredzone.acme4j.challenge.Dns01Challenge; 037import org.shredzone.acme4j.challenge.Http01Challenge; 038import org.shredzone.acme4j.connector.Resource; 039import org.shredzone.acme4j.exception.AcmeException; 040import org.shredzone.acme4j.exception.AcmeNotSupportedException; 041import org.shredzone.acme4j.exception.AcmeServerException; 042import org.shredzone.acme4j.provider.TestableConnectionProvider; 043import org.shredzone.acme4j.toolbox.JSON; 044import org.shredzone.acme4j.toolbox.JSONBuilder; 045import org.shredzone.acme4j.toolbox.TestUtils; 046 047/** 048 * Unit tests for {@link Account}. 049 */ 050public class AccountTest { 051 052 private final URL resourceUrl = url("http://example.com/acme/resource"); 053 private final URL locationUrl = url(TestUtils.ACCOUNT_URL); 054 private final URL agreementUrl = url("http://example.com/agreement.pdf"); 055 056 /** 057 * Test that a account can be updated. 058 */ 059 @Test 060 public void testUpdateAccount() throws AcmeException, IOException { 061 var provider = new TestableConnectionProvider() { 062 private JSON jsonResponse; 063 064 @Override 065 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 066 assertThat(url).isEqualTo(locationUrl); 067 assertThatJson(claims.toString()).isEqualTo(getJSON("updateAccount").toString()); 068 assertThat(login).isNotNull(); 069 jsonResponse = getJSON("updateAccountResponse"); 070 return HttpURLConnection.HTTP_OK; 071 } 072 073 @Override 074 public int sendSignedPostAsGetRequest(URL url, Login login) { 075 if ("https://example.com/acme/acct/1/orders".equals(url.toExternalForm())) { 076 jsonResponse = new JSONBuilder() 077 .array("orders", singletonList("https://example.com/acme/order/1")) 078 .toJSON(); 079 } else { 080 jsonResponse = getJSON("updateAccountResponse"); 081 } 082 return HttpURLConnection.HTTP_OK; 083 } 084 085 @Override 086 public JSON readJsonResponse() { 087 return jsonResponse; 088 } 089 090 @Override 091 public URL getLocation() { 092 return locationUrl; 093 } 094 095 @Override 096 public Collection<URL> getLinks(String relation) { 097 return emptyList(); 098 } 099 }; 100 101 var login = provider.createLogin(); 102 var account = new Account(login); 103 account.update(); 104 105 assertThat(login.getAccountLocation()).isEqualTo(locationUrl); 106 assertThat(account.getLocation()).isEqualTo(locationUrl); 107 assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue(); 108 assertThat(account.getContacts()).hasSize(1); 109 assertThat(account.getContacts().get(0)).isEqualTo(URI.create("mailto:foo2@example.com")); 110 assertThat(account.getStatus()).isEqualTo(Status.VALID); 111 assertThat(account.hasExternalAccountBinding()).isTrue(); 112 assertThat(account.getKeyIdentifier().orElseThrow()).isEqualTo("NCC-1701"); 113 114 var orderIt = account.getOrders(); 115 assertThat(orderIt).isNotNull(); 116 assertThat(orderIt.next().getLocation()).isEqualTo(url("https://example.com/acme/order/1")); 117 assertThat(orderIt.hasNext()).isFalse(); 118 119 provider.close(); 120 } 121 122 /** 123 * Test lazy loading. 124 */ 125 @Test 126 public void testLazyLoading() throws IOException { 127 var requestWasSent = new AtomicBoolean(false); 128 129 var provider = new TestableConnectionProvider() { 130 @Override 131 public int sendSignedPostAsGetRequest(URL url, Login login) { 132 requestWasSent.set(true); 133 assertThat(url).isEqualTo(locationUrl); 134 return HttpURLConnection.HTTP_OK; 135 } 136 137 @Override 138 public JSON readJsonResponse() { 139 return getJSON("updateAccountResponse"); 140 } 141 142 @Override 143 public URL getLocation() { 144 return locationUrl; 145 } 146 147 @Override 148 public Collection<URL> getLinks(String relation) { 149 switch(relation) { 150 case "termsOfService": return singletonList(agreementUrl); 151 default: return emptyList(); 152 } 153 } 154 }; 155 156 var account = new Account(provider.createLogin()); 157 158 // Lazy loading 159 assertThat(requestWasSent.get()).isFalse(); 160 assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue(); 161 assertThat(requestWasSent.get()).isTrue(); 162 163 // Subsequent queries do not trigger another load 164 requestWasSent.set(false); 165 assertThat(account.getTermsOfServiceAgreed().orElseThrow()).isTrue(); 166 assertThat(account.getStatus()).isEqualTo(Status.VALID); 167 assertThat(requestWasSent.get()).isFalse(); 168 169 provider.close(); 170 } 171 172 /** 173 * Test that a domain can be pre-authorized. 174 */ 175 @Test 176 public void testPreAuthorizeDomain() throws Exception { 177 var provider = new TestableConnectionProvider() { 178 @Override 179 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 180 assertThat(url).isEqualTo(resourceUrl); 181 assertThatJson(claims.toString()).isEqualTo(getJSON("newAuthorizationRequest").toString()); 182 assertThat(login).isNotNull(); 183 return HttpURLConnection.HTTP_CREATED; 184 } 185 186 @Override 187 public JSON readJsonResponse() { 188 return getJSON("newAuthorizationResponse"); 189 } 190 191 @Override 192 public URL getLocation() { 193 return locationUrl; 194 } 195 }; 196 197 var login = provider.createLogin(); 198 199 provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl); 200 provider.putTestChallenge(Http01Challenge.TYPE, Http01Challenge::new); 201 provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new); 202 203 var domainName = "example.org"; 204 205 var account = new Account(login); 206 var auth = account.preAuthorize(Identifier.dns(domainName)); 207 208 assertThat(auth.getIdentifier().getDomain()).isEqualTo(domainName); 209 assertThat(auth.getStatus()).isEqualTo(Status.PENDING); 210 assertThat(auth.getExpires()).isEmpty(); 211 assertThat(auth.getLocation()).isEqualTo(locationUrl); 212 213 assertThat(auth.getChallenges()).containsExactlyInAnyOrder( 214 provider.getChallenge(Http01Challenge.TYPE), 215 provider.getChallenge(Dns01Challenge.TYPE)); 216 217 provider.close(); 218 } 219 220 /** 221 * Test that pre-authorization with subdomains fails if not supported. 222 */ 223 @Test 224 public void testPreAuthorizeDomainSubdomainsFails() throws Exception { 225 var provider = new TestableConnectionProvider(); 226 227 var login = provider.createLogin(); 228 229 provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl); 230 231 assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isFalse(); 232 233 var account = new Account(login); 234 235 assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> 236 account.preAuthorize(Identifier.dns("example.org").allowSubdomainAuth()) 237 ); 238 239 provider.close(); 240 } 241 242 /** 243 * Test that a domain can be pre-authorized, with allowed subdomains. 244 */ 245 @Test 246 public void testPreAuthorizeDomainSubdomains() throws Exception { 247 var provider = new TestableConnectionProvider() { 248 @Override 249 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 250 assertThat(url).isEqualTo(resourceUrl); 251 assertThatJson(claims.toString()).isEqualTo(getJSON("newAuthorizationRequestSub").toString()); 252 assertThat(login).isNotNull(); 253 return HttpURLConnection.HTTP_CREATED; 254 } 255 256 @Override 257 public JSON readJsonResponse() { 258 return getJSON("newAuthorizationResponseSub"); 259 } 260 261 @Override 262 public URL getLocation() { 263 return locationUrl; 264 } 265 }; 266 267 var login = provider.createLogin(); 268 269 provider.putMetadata("subdomainAuthAllowed", true); 270 provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl); 271 provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new); 272 273 var domainName = "example.org"; 274 275 var account = new Account(login); 276 var auth = account.preAuthorize(Identifier.dns(domainName).allowSubdomainAuth()); 277 278 assertThat(login.getSession().getMetadata().isSubdomainAuthAllowed()).isTrue(); 279 assertThat(auth.getIdentifier().getDomain()).isEqualTo(domainName); 280 assertThat(auth.getStatus()).isEqualTo(Status.PENDING); 281 assertThat(auth.getExpires()).isEmpty(); 282 assertThat(auth.getLocation()).isEqualTo(locationUrl); 283 assertThat(auth.isSubdomainAuthAllowed()).isTrue(); 284 285 assertThat(auth.getChallenges()).containsExactlyInAnyOrder( 286 provider.getChallenge(Dns01Challenge.TYPE)); 287 288 provider.close(); 289 } 290 291 /** 292 * Test that a domain pre-authorization can fail. 293 */ 294 @Test 295 public void testNoPreAuthorizeDomain() throws Exception { 296 var problemType = URI.create("urn:ietf:params:acme:error:rejectedIdentifier"); 297 var problemDetail = "example.org is blacklisted"; 298 299 var provider = new TestableConnectionProvider() { 300 @Override 301 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) throws AcmeException { 302 assertThat(url).isEqualTo(resourceUrl); 303 assertThatJson(claims.toString()).isEqualTo(getJSON("newAuthorizationRequest").toString()); 304 assertThat(login).isNotNull(); 305 306 var problem = TestUtils.createProblem(problemType, problemDetail, resourceUrl); 307 throw new AcmeServerException(problem); 308 } 309 }; 310 311 var login = provider.createLogin(); 312 313 provider.putTestResource(Resource.NEW_AUTHZ, resourceUrl); 314 315 var account = new Account(login); 316 317 var ex = assertThrows(AcmeServerException.class, () -> 318 account.preAuthorizeDomain("example.org") 319 ); 320 assertThat(ex.getType()).isEqualTo(problemType); 321 assertThat(ex.getMessage()).isEqualTo(problemDetail); 322 323 provider.close(); 324 } 325 326 /** 327 * Test that a bad domain parameter is not accepted. 328 */ 329 @Test 330 public void testAuthorizeBadDomain() throws Exception { 331 var provider = new TestableConnectionProvider(); 332 // just provide a resource record so the provider returns a directory 333 provider.putTestResource(Resource.NEW_NONCE, resourceUrl); 334 335 var login = provider.createLogin(); 336 var account = login.getAccount(); 337 338 assertThatNullPointerException() 339 .isThrownBy(() -> account.preAuthorizeDomain(null)); 340 assertThatIllegalArgumentException() 341 .isThrownBy(() -> account.preAuthorizeDomain("")); 342 assertThatExceptionOfType(AcmeNotSupportedException.class) 343 .isThrownBy(() -> account.preAuthorizeDomain("example.com")) 344 .withMessage("Server does not support newAuthz"); 345 346 provider.close(); 347 } 348 349 /** 350 * Test that the account key can be changed. 351 */ 352 @Test 353 public void testChangeKey() throws Exception { 354 var oldKeyPair = TestUtils.createKeyPair(); 355 var newKeyPair = TestUtils.createDomainKeyPair(); 356 357 var provider = new TestableConnectionProvider() { 358 @Override 359 public int sendSignedRequest(URL url, JSONBuilder payload, Login login) { 360 try { 361 assertThat(url).isEqualTo(locationUrl); 362 assertThat(login).isNotNull(); 363 assertThat(login.getKeyPair()).isSameAs(oldKeyPair); 364 365 var json = payload.toJSON(); 366 var encodedHeader = json.get("protected").asString(); 367 var encodedSignature = json.get("signature").asString(); 368 var encodedPayload = json.get("payload").asString(); 369 370 var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature); 371 var jws = new JsonWebSignature(); 372 jws.setCompactSerialization(serialized); 373 jws.setKey(newKeyPair.getPublic()); 374 assertThat(jws.verifySignature()).isTrue(); 375 376 var decodedPayload = jws.getPayload(); 377 378 var expectedPayload = new StringBuilder(); 379 expectedPayload.append('{'); 380 expectedPayload.append("\"account\":\"").append(locationUrl).append("\","); 381 expectedPayload.append("\"oldKey\":{"); 382 expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\","); 383 expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\","); 384 expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\""); 385 expectedPayload.append("}}"); 386 assertThatJson(decodedPayload).isEqualTo(expectedPayload.toString()); 387 } catch (JoseException ex) { 388 fail(ex); 389 } 390 391 return HttpURLConnection.HTTP_OK; 392 } 393 394 @Override 395 public URL getLocation() { 396 return locationUrl; 397 } 398 }; 399 400 provider.putTestResource(Resource.KEY_CHANGE, locationUrl); 401 402 var session = TestUtils.session(provider); 403 var login = new Login(locationUrl, oldKeyPair, session); 404 405 assertThat(login.getKeyPair()).isSameAs(oldKeyPair); 406 407 var account = new Account(login); 408 account.changeKey(newKeyPair); 409 410 assertThat(login.getKeyPair()).isSameAs(newKeyPair); 411 } 412 413 /** 414 * Test that the same account key is not accepted for change. 415 */ 416 @Test 417 public void testChangeSameKey() { 418 assertThrows(IllegalArgumentException.class, () -> { 419 var provider = new TestableConnectionProvider(); 420 var login = provider.createLogin(); 421 422 var account = new Account(login); 423 account.changeKey(login.getKeyPair()); 424 425 provider.close(); 426 }); 427 } 428 429 /** 430 * Test that an account can be deactivated. 431 */ 432 @Test 433 public void testDeactivate() throws Exception { 434 var provider = new TestableConnectionProvider() { 435 @Override 436 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 437 var json = claims.toJSON(); 438 assertThat(json.get("status").asString()).isEqualTo("deactivated"); 439 assertThat(url).isEqualTo(locationUrl); 440 assertThat(login).isNotNull(); 441 return HttpURLConnection.HTTP_OK; 442 } 443 444 @Override 445 public JSON readJsonResponse() { 446 return getJSON("deactivateAccountResponse"); 447 } 448 }; 449 450 var account = new Account(provider.createLogin()); 451 account.deactivate(); 452 453 assertThat(account.getStatus()).isEqualTo(Status.DEACTIVATED); 454 455 provider.close(); 456 } 457 458 /** 459 * Test that a new order can be created. 460 */ 461 @Test 462 public void testNewOrder() throws AcmeException, IOException { 463 var provider = new TestableConnectionProvider(); 464 var login = provider.createLogin(); 465 466 var account = new Account(login); 467 assertThat(account.newOrder()).isNotNull(); 468 469 provider.close(); 470 } 471 472 /** 473 * Test that an account can be modified. 474 */ 475 @Test 476 public void testModify() throws Exception { 477 var provider = new TestableConnectionProvider() { 478 @Override 479 public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { 480 assertThat(url).isEqualTo(locationUrl); 481 assertThatJson(claims.toString()).isEqualTo(getJSON("modifyAccount").toString()); 482 assertThat(login).isNotNull(); 483 return HttpURLConnection.HTTP_OK; 484 } 485 486 @Override 487 public JSON readJsonResponse() { 488 return getJSON("modifyAccountResponse"); 489 } 490 491 @Override 492 public URL getLocation() { 493 return locationUrl; 494 } 495 }; 496 497 var account = new Account(provider.createLogin()); 498 account.setJSON(getJSON("newAccount")); 499 500 var editable = account.modify(); 501 assertThat(editable).isNotNull(); 502 503 editable.addContact("mailto:foo2@example.com"); 504 editable.getContacts().add(URI.create("mailto:foo3@example.com")); 505 editable.commit(); 506 507 assertThat(account.getLocation()).isEqualTo(locationUrl); 508 assertThat(account.getContacts()).hasSize(3); 509 assertThat(account.getContacts()).element(0).isEqualTo(URI.create("mailto:foo@example.com")); 510 assertThat(account.getContacts()).element(1).isEqualTo(URI.create("mailto:foo2@example.com")); 511 assertThat(account.getContacts()).element(2).isEqualTo(URI.create("mailto:foo3@example.com")); 512 513 provider.close(); 514 } 515 516}