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}