001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2019 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.toolbox; 015 016import static java.nio.charset.StandardCharsets.UTF_8; 017import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; 018import static org.assertj.core.api.Assertions.assertThat; 019import static org.junit.jupiter.api.Assertions.fail; 020import static org.shredzone.acme4j.toolbox.TestUtils.url; 021 022import java.net.URL; 023import java.util.Base64; 024import java.util.HashMap; 025 026import javax.crypto.SecretKey; 027 028import org.jose4j.jwk.PublicJsonWebKey; 029import org.jose4j.jws.JsonWebSignature; 030import org.jose4j.jwx.CompactSerializer; 031import org.jose4j.lang.JoseException; 032import org.junit.jupiter.api.Test; 033import org.junit.jupiter.params.ParameterizedTest; 034import org.junit.jupiter.params.provider.CsvSource; 035 036/** 037 * Unit tests for {@link JoseUtils}. 038 */ 039public class JoseUtilsTest { 040 041 private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding(); 042 private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder(); 043 044 /** 045 * Test if a JOSE ACME POST request is correctly created. 046 */ 047 @Test 048 public void testCreateJosePostRequest() throws Exception { 049 var resourceUrl = url("http://example.com/acme/resource"); 050 var accountKey = TestUtils.createKeyPair(); 051 var nonce = URL_ENCODER.encodeToString("foo-nonce-1-foo".getBytes()); 052 var payload = new JSONBuilder(); 053 payload.put("foo", 123); 054 payload.put("bar", "a-string"); 055 056 var jose = JoseUtils 057 .createJoseRequest(resourceUrl, accountKey, payload, nonce, TestUtils.ACCOUNT_URL) 058 .toMap(); 059 060 var encodedHeader = jose.get("protected").toString(); 061 var encodedSignature = jose.get("signature").toString(); 062 var encodedPayload = jose.get("payload").toString(); 063 064 var expectedHeader = new StringBuilder(); 065 expectedHeader.append('{'); 066 expectedHeader.append("\"nonce\":\"").append(nonce).append("\","); 067 expectedHeader.append("\"url\":\"").append(resourceUrl).append("\","); 068 expectedHeader.append("\"alg\":\"RS256\","); 069 expectedHeader.append("\"kid\":\"").append(TestUtils.ACCOUNT_URL).append('"'); 070 expectedHeader.append('}'); 071 072 assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8)) 073 .isEqualTo(expectedHeader.toString()); 074 assertThatJson(new String(URL_DECODER.decode(encodedPayload), UTF_8)) 075 .isEqualTo("{\"foo\":123,\"bar\":\"a-string\"}"); 076 assertThat(encodedSignature).isNotEmpty(); 077 078 var jws = new JsonWebSignature(); 079 jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature)); 080 jws.setKey(accountKey.getPublic()); 081 assertThat(jws.verifySignature()).isTrue(); 082 } 083 084 /** 085 * Test if a JOSE ACME POST-as-GET request is correctly created. 086 */ 087 @Test 088 public void testCreateJosePostAsGetRequest() throws Exception { 089 var resourceUrl = url("http://example.com/acme/resource"); 090 var accountKey = TestUtils.createKeyPair(); 091 var nonce = URL_ENCODER.encodeToString("foo-nonce-1-foo".getBytes()); 092 093 var jose = JoseUtils 094 .createJoseRequest(resourceUrl, accountKey, null, nonce, TestUtils.ACCOUNT_URL) 095 .toMap(); 096 097 var encodedHeader = jose.get("protected").toString(); 098 var encodedSignature = jose.get("signature").toString(); 099 var encodedPayload = jose.get("payload").toString(); 100 101 var expectedHeader = new StringBuilder(); 102 expectedHeader.append('{'); 103 expectedHeader.append("\"nonce\":\"").append(nonce).append("\","); 104 expectedHeader.append("\"url\":\"").append(resourceUrl).append("\","); 105 expectedHeader.append("\"alg\":\"RS256\","); 106 expectedHeader.append("\"kid\":\"").append(TestUtils.ACCOUNT_URL).append('"'); 107 expectedHeader.append('}'); 108 109 assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8)) 110 .isEqualTo(expectedHeader.toString()); 111 assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8)).isEmpty(); 112 assertThat(encodedSignature).isNotEmpty(); 113 114 var jws = new JsonWebSignature(); 115 jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature)); 116 jws.setKey(accountKey.getPublic()); 117 assertThat(jws.verifySignature()).isTrue(); 118 } 119 120 /** 121 * Test if a JOSE ACME Key-Change request is correctly created. 122 */ 123 @Test 124 public void testCreateJoseKeyChangeRequest() throws Exception { 125 var resourceUrl = url("http://example.com/acme/resource"); 126 var accountKey = TestUtils.createKeyPair(); 127 var payload = new JSONBuilder(); 128 payload.put("foo", 123); 129 payload.put("bar", "a-string"); 130 131 var jose = JoseUtils 132 .createJoseRequest(resourceUrl, accountKey, payload, null, null) 133 .toMap(); 134 135 var encodedHeader = jose.get("protected").toString(); 136 var encodedSignature = jose.get("signature").toString(); 137 var encodedPayload = jose.get("payload").toString(); 138 139 var expectedHeader = new StringBuilder(); 140 expectedHeader.append('{'); 141 expectedHeader.append("\"url\":\"").append(resourceUrl).append("\","); 142 expectedHeader.append("\"alg\":\"RS256\","); 143 expectedHeader.append("\"jwk\": {"); 144 expectedHeader.append("\"kty\": \"").append(TestUtils.KTY).append("\","); 145 expectedHeader.append("\"e\": \"").append(TestUtils.E).append("\","); 146 expectedHeader.append("\"n\": \"").append(TestUtils.N).append("\"}"); 147 expectedHeader.append("}"); 148 149 assertThatJson(new String(URL_DECODER.decode(encodedHeader), UTF_8)) 150 .isEqualTo(expectedHeader.toString()); 151 assertThatJson(new String(URL_DECODER.decode(encodedPayload), UTF_8)) 152 .isEqualTo("{\"foo\":123,\"bar\":\"a-string\"}"); 153 assertThat(encodedSignature).isNotEmpty(); 154 155 var jws = new JsonWebSignature(); 156 jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature)); 157 jws.setKey(accountKey.getPublic()); 158 assertThat(jws.verifySignature()).isTrue(); 159 } 160 161 /** 162 * Test if an external account binding is correctly created. 163 */ 164 @ParameterizedTest 165 @CsvSource({"SHA-256,HS256", "SHA-384,HS384", "SHA-512,HS512", "SHA-512,HS256"}) 166 public void testCreateExternalAccountBinding(String keyAlg, String macAlg) throws Exception { 167 var accountKey = TestUtils.createKeyPair(); 168 var keyIdentifier = "NCC-1701"; 169 var macKey = TestUtils.createSecretKey(keyAlg); 170 var resourceUrl = url("http://example.com/acme/resource"); 171 172 var binding = JoseUtils.createExternalAccountBinding( 173 keyIdentifier, accountKey.getPublic(), macKey, macAlg, resourceUrl); 174 175 var encodedHeader = binding.get("protected").toString(); 176 var encodedSignature = binding.get("signature").toString(); 177 var encodedPayload = binding.get("payload").toString(); 178 var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature); 179 180 assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, macAlg); 181 } 182 183 /** 184 * Test if public key is correctly converted to JWK structure. 185 */ 186 @Test 187 public void testPublicKeyToJWK() throws Exception { 188 var json = JoseUtils.publicKeyToJWK(TestUtils.createKeyPair().getPublic()); 189 assertThat(json).hasSize(3); 190 assertThat(json.get("kty")).isEqualTo(TestUtils.KTY); 191 assertThat(json.get("n")).isEqualTo(TestUtils.N); 192 assertThat(json.get("e")).isEqualTo(TestUtils.E); 193 } 194 195 /** 196 * Test if JWK structure is correctly converted to public key. 197 */ 198 @Test 199 public void testJWKToPublicKey() throws Exception { 200 var json = new HashMap<String, Object>(); 201 json.put("kty", TestUtils.KTY); 202 json.put("n", TestUtils.N); 203 json.put("e", TestUtils.E); 204 var key = JoseUtils.jwkToPublicKey(json); 205 assertThat(key.getEncoded()).isEqualTo(TestUtils.createKeyPair().getPublic().getEncoded()); 206 } 207 208 /** 209 * Test if thumbprint is correctly computed. 210 */ 211 @Test 212 public void testThumbprint() throws Exception { 213 var thumb = JoseUtils.thumbprint(TestUtils.createKeyPair().getPublic()); 214 var encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(thumb); 215 assertThat(encoded).isEqualTo(TestUtils.THUMBPRINT); 216 } 217 218 /** 219 * Test if RSA using SHA-256 keys are properly detected. 220 */ 221 @Test 222 public void testRsaKey() throws Exception { 223 var rsaKeyPair = TestUtils.createKeyPair(); 224 var jwk = PublicJsonWebKey.Factory.newPublicJwk(rsaKeyPair.getPublic()); 225 226 var type = JoseUtils.keyAlgorithm(jwk); 227 assertThat(type).isEqualTo("RS256"); 228 } 229 230 /** 231 * Test if ECDSA using NIST P-256 curve and SHA-256 keys are properly detected. 232 */ 233 @Test 234 public void testP256ECKey() throws Exception { 235 var ecKeyPair = TestUtils.createECKeyPair("secp256r1"); 236 var jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); 237 238 var type = JoseUtils.keyAlgorithm(jwk); 239 assertThat(type).isEqualTo("ES256"); 240 } 241 242 /** 243 * Test if ECDSA using NIST P-384 curve and SHA-384 keys are properly detected. 244 */ 245 @Test 246 public void testP384ECKey() throws Exception { 247 var ecKeyPair = TestUtils.createECKeyPair("secp384r1"); 248 var jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); 249 250 var type = JoseUtils.keyAlgorithm(jwk); 251 assertThat(type).isEqualTo("ES384"); 252 } 253 254 /** 255 * Test if ECDSA using NIST P-521 curve and SHA-512 keys are properly detected. 256 */ 257 @Test 258 public void testP521ECKey() throws Exception { 259 var ecKeyPair = TestUtils.createECKeyPair("secp521r1"); 260 var jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); 261 262 var type = JoseUtils.keyAlgorithm(jwk); 263 assertThat(type).isEqualTo("ES512"); 264 } 265 266 /** 267 * Test if MAC key algorithms are properly detected. 268 */ 269 @Test 270 public void testMacKey() throws Exception { 271 assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey("SHA-256"))).isEqualTo("HS256"); 272 assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey("SHA-384"))).isEqualTo("HS384"); 273 assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey("SHA-512"))).isEqualTo("HS512"); 274 } 275 276 /** 277 * Asserts that the serialized external account binding is valid. Unit test fails if 278 * the account binding is invalid. 279 * 280 * @param serialized 281 * Serialized external account binding JOSE structure 282 * @param resourceUrl 283 * Expected resource {@link URL} 284 * @param keyIdentifier 285 * Expected key identifier 286 * @param macKey 287 * Expected {@link SecretKey} 288 * @param macAlg 289 * Expected algorithm 290 */ 291 public static void assertExternalAccountBinding(String serialized, URL resourceUrl, 292 String keyIdentifier, SecretKey macKey, 293 String macAlg) { 294 try { 295 var jws = new JsonWebSignature(); 296 jws.setCompactSerialization(serialized); 297 jws.setKey(macKey); 298 assertThat(jws.verifySignature()).isTrue(); 299 300 assertThat(jws.getHeader("url")).isEqualTo(resourceUrl.toString()); 301 assertThat(jws.getHeader("kid")).isEqualTo(keyIdentifier); 302 assertThat(jws.getHeader("alg")).isEqualTo(macAlg); 303 304 var decodedPayload = jws.getPayload(); 305 var expectedPayload = new StringBuilder(); 306 expectedPayload.append('{'); 307 expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\","); 308 expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\","); 309 expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\""); 310 expectedPayload.append("}"); 311 assertThatJson(decodedPayload).isEqualTo(expectedPayload.toString()); 312 } catch (JoseException ex) { 313 fail(ex); 314 } 315 } 316 317}