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.toolbox; 015 016import static org.assertj.core.api.Assertions.assertThat; 017import static org.assertj.core.api.Assertions.within; 018import static org.junit.jupiter.api.Assertions.assertThrows; 019import static org.shredzone.acme4j.toolbox.AcmeUtils.*; 020 021import java.io.ByteArrayOutputStream; 022import java.io.IOException; 023import java.io.OutputStreamWriter; 024import java.io.Writer; 025import java.lang.reflect.Modifier; 026import java.net.URI; 027import java.security.Security; 028import java.security.cert.CertificateEncodingException; 029import java.time.temporal.ChronoUnit; 030import java.util.Locale; 031import java.util.stream.Stream; 032 033import org.bouncycastle.jce.provider.BouncyCastleProvider; 034import org.junit.jupiter.api.BeforeAll; 035import org.junit.jupiter.api.Test; 036import org.junit.jupiter.params.ParameterizedTest; 037import org.junit.jupiter.params.provider.Arguments; 038import org.junit.jupiter.params.provider.MethodSource; 039import org.junit.jupiter.params.provider.NullSource; 040import org.junit.jupiter.params.provider.ValueSource; 041import org.shredzone.acme4j.exception.AcmeProtocolException; 042 043/** 044 * Unit tests for {@link AcmeUtils}. 045 */ 046public class AcmeUtilsTest { 047 048 @BeforeAll 049 public static void setup() { 050 Security.addProvider(new BouncyCastleProvider()); 051 } 052 053 /** 054 * Test that constructor is private. 055 */ 056 @Test 057 public void testPrivateConstructor() throws Exception { 058 var constructor = AcmeUtils.class.getDeclaredConstructor(); 059 assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue(); 060 constructor.setAccessible(true); 061 constructor.newInstance(); 062 } 063 064 /** 065 * Test sha-256 hash and hex encode. 066 */ 067 @Test 068 public void testSha256HashHexEncode() { 069 var hexEncode = hexEncode(sha256hash("foobar")); 070 assertThat(hexEncode).isEqualTo("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"); 071 } 072 073 /** 074 * Test base64 URL encode. 075 */ 076 @Test 077 public void testBase64UrlEncode() { 078 var base64UrlEncode = base64UrlEncode(sha256hash("foobar")); 079 assertThat(base64UrlEncode).isEqualTo("w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"); 080 } 081 082 /** 083 * Test base64 URL decode. 084 */ 085 @Test 086 public void testBase64UrlDecode() { 087 var base64UrlDecode = base64UrlDecode("w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"); 088 assertThat(base64UrlDecode).isEqualTo(sha256hash("foobar")); 089 } 090 091 /** 092 * Test base64 URL validation for valid values 093 */ 094 @ParameterizedTest 095 @ValueSource(strings = { 096 "", 097 "Zg", 098 "Zm9v", 099 }) 100 public void testBase64UrlValid(String url) { 101 assertThat(isValidBase64Url(url)).isTrue(); 102 } 103 104 /** 105 * Test base64 URL validation for invalid values 106 */ 107 @ParameterizedTest 108 @ValueSource(strings = { 109 " ", 110 "Zg=", 111 "Zg==", 112 " Zm9v ", 113 "<some>.illegal#Text", 114 }) 115 @NullSource 116 public void testBase64UrlInvalid(String url) { 117 assertThat(isValidBase64Url(url)).isFalse(); 118 } 119 120 /** 121 * Test ACE conversion. 122 */ 123 @Test 124 public void testToAce() { 125 // Test ASCII domains in different notations 126 assertThat(toAce("example.com")).isEqualTo("example.com"); 127 assertThat(toAce(" example.com ")).isEqualTo("example.com"); 128 assertThat(toAce("ExAmPlE.CoM")).isEqualTo("example.com"); 129 assertThat(toAce("foo.example.com")).isEqualTo("foo.example.com"); 130 assertThat(toAce("bar.foo.example.com")).isEqualTo("bar.foo.example.com"); 131 132 // Test IDN domains 133 assertThat(toAce("ExÄmþle.¢öM")).isEqualTo("xn--exmle-hra7p.xn--m-7ba6w"); 134 135 // Test alternate separators 136 assertThat(toAce("example\u3002com")).isEqualTo("example.com"); 137 assertThat(toAce("example\uff0ecom")).isEqualTo("example.com"); 138 assertThat(toAce("example\uff61com")).isEqualTo("example.com"); 139 140 // Test ACE encoded domains, they must not change 141 assertThat(toAce("xn--exmle-hra7p.xn--m-7ba6w")) 142 .isEqualTo("xn--exmle-hra7p.xn--m-7ba6w"); 143 } 144 145 /** 146 * Test valid strings. 147 */ 148 @ParameterizedTest 149 @MethodSource("provideTimestamps") 150 public void testParser(String input, String expected) { 151 Arguments.of(input, expected, within(1, ChronoUnit.MILLIS)); 152 } 153 154 private static Stream<Arguments> provideTimestamps() { 155 return Stream.of( 156 Arguments.of("2015-12-27T22:58:35.006769519Z", "2015-12-27T22:58:35.006Z"), 157 Arguments.of("2015-12-27T22:58:35.00676951Z", "2015-12-27T22:58:35.006Z"), 158 Arguments.of("2015-12-27T22:58:35.0067695Z", "2015-12-27T22:58:35.006Z"), 159 Arguments.of("2015-12-27T22:58:35.006769Z", "2015-12-27T22:58:35.006Z"), 160 Arguments.of("2015-12-27T22:58:35.00676Z", "2015-12-27T22:58:35.006Z"), 161 Arguments.of("2015-12-27T22:58:35.0067Z", "2015-12-27T22:58:35.006Z"), 162 Arguments.of("2015-12-27T22:58:35.006Z", "2015-12-27T22:58:35.006Z"), 163 Arguments.of("2015-12-27T22:58:35.01Z", "2015-12-27T22:58:35.010Z"), 164 Arguments.of("2015-12-27T22:58:35.2Z", "2015-12-27T22:58:35.200Z"), 165 Arguments.of("2015-12-27T22:58:35Z", "2015-12-27T22:58:35.000Z"), 166 Arguments.of("2015-12-27t22:58:35z", "2015-12-27T22:58:35.000Z"), 167 168 Arguments.of("2015-12-27T22:58:35.006769519+02:00", "2015-12-27T20:58:35.006Z"), 169 Arguments.of("2015-12-27T22:58:35.006+02:00", "2015-12-27T20:58:35.006Z"), 170 Arguments.of("2015-12-27T22:58:35+02:00", "2015-12-27T20:58:35.000Z"), 171 172 Arguments.of("2015-12-27T21:58:35.006769519-02:00", "2015-12-27T23:58:35.006Z"), 173 Arguments.of("2015-12-27T21:58:35.006-02:00", "2015-12-27T23:58:35.006Z"), 174 Arguments.of("2015-12-27T21:58:35-02:00", "2015-12-27T23:58:35.000Z"), 175 176 Arguments.of("2015-12-27T22:58:35.006769519+0200", "2015-12-27T20:58:35.006Z"), 177 Arguments.of("2015-12-27T22:58:35.006+0200", "2015-12-27T20:58:35.006Z"), 178 Arguments.of("2015-12-27T22:58:35+0200", "2015-12-27T20:58:35.000Z"), 179 180 Arguments.of("2015-12-27T21:58:35.006769519-0200", "2015-12-27T23:58:35.006Z"), 181 Arguments.of("2015-12-27T21:58:35.006-0200", "2015-12-27T23:58:35.006Z"), 182 Arguments.of("2015-12-27T21:58:35-0200", "2015-12-27T23:58:35.000Z") 183 ); 184 } 185 186 /** 187 * Test invalid strings. 188 */ 189 @Test 190 public void testInvalid() { 191 assertThrows(IllegalArgumentException.class, 192 () -> parseTimestamp(""), 193 "accepted empty string"); 194 assertThrows(IllegalArgumentException.class, 195 () -> parseTimestamp("abc"), 196 "accepted nonsense string"); 197 assertThrows(IllegalArgumentException.class, 198 () -> parseTimestamp("2015-12-27"), 199 "accepted date only string"); 200 assertThrows(IllegalArgumentException.class, 201 () -> parseTimestamp("2015-12-27T"), 202 "accepted string without time"); 203 } 204 205 /** 206 * Test that locales are correctly converted to language headers. 207 */ 208 @Test 209 public void testLocaleToLanguageHeader() { 210 assertThat(localeToLanguageHeader(Locale.ENGLISH)) 211 .isEqualTo("en,*;q=0.1"); 212 assertThat(localeToLanguageHeader(new Locale("en", "US"))) 213 .isEqualTo("en-US,en;q=0.8,*;q=0.1"); 214 assertThat(localeToLanguageHeader(Locale.GERMAN)) 215 .isEqualTo("de,*;q=0.1"); 216 assertThat(localeToLanguageHeader(Locale.GERMANY)) 217 .isEqualTo("de-DE,de;q=0.8,*;q=0.1"); 218 assertThat(localeToLanguageHeader(new Locale(""))) 219 .isEqualTo("*"); 220 assertThat(localeToLanguageHeader(null)) 221 .isEqualTo("*"); 222 } 223 224 /** 225 * Test that error prefix is correctly removed. 226 */ 227 @Test 228 public void testStripErrorPrefix() { 229 assertThat(stripErrorPrefix("urn:ietf:params:acme:error:unauthorized")).isEqualTo("unauthorized"); 230 assertThat(stripErrorPrefix("urn:somethingelse:error:message")).isNull(); 231 assertThat(stripErrorPrefix(null)).isNull(); 232 } 233 234 /** 235 * Test that {@link AcmeUtils#writeToPem(byte[], PemLabel, Writer)} writes a correct PEM 236 * file. 237 */ 238 @Test 239 public void testWriteToPem() throws IOException, CertificateEncodingException { 240 var certChain = TestUtils.createCertificate("/cert.pem"); 241 242 var pemFile = new ByteArrayOutputStream(); 243 try (var w = new OutputStreamWriter(pemFile)) { 244 for (var cert : certChain) { 245 AcmeUtils.writeToPem(cert.getEncoded(), AcmeUtils.PemLabel.CERTIFICATE, w); 246 } 247 } 248 249 var originalFile = new ByteArrayOutputStream(); 250 try (var in = getClass().getResourceAsStream("/cert.pem")) { 251 var buffer = new byte[2048]; 252 int len; 253 while ((len = in.read(buffer)) >= 0) { 254 originalFile.write(buffer, 0, len); 255 } 256 } 257 258 assertThat(pemFile.toByteArray()).isEqualTo(originalFile.toByteArray()); 259 } 260 261 /** 262 * Test {@link AcmeUtils#getContentType(String)} for JSON types. 263 */ 264 @ParameterizedTest 265 @ValueSource(strings = { 266 "application/json", 267 "application/json; charset=utf-8", 268 "application/json; charset=utf-8 (Plain text)", 269 "application/json; charset=\"utf-8\"", 270 "application/json; charset=\"UTF-8\"; foo=4", 271 " application/json ;foo=4", 272 }) 273 public void testGetContentTypeForJson(String contentType) { 274 assertThat(AcmeUtils.getContentType(contentType)).isEqualTo("application/json"); 275 } 276 277 /** 278 * Test {@link AcmeUtils#getContentType(String)} with other types. 279 */ 280 @Test 281 public void testGetContentType() { 282 assertThat(AcmeUtils.getContentType(null)).isNull(); 283 assertThat(AcmeUtils.getContentType("Application/Problem+JSON")) 284 .isEqualTo("application/problem+json"); 285 assertThrows(AcmeProtocolException.class, 286 () -> AcmeUtils.getContentType("application/json; charset=\"iso-8859-1\"")); 287 } 288 289 /** 290 * Test that {@link AcmeUtils#validateContact(java.net.URI)} refuses invalid 291 * contacts. 292 */ 293 @Test 294 public void testValidateContact() { 295 AcmeUtils.validateContact(URI.create("mailto:foo@example.com")); 296 297 assertThrows(IllegalArgumentException.class, 298 () -> AcmeUtils.validateContact(URI.create("mailto:foo@example.com,bar@example.com")), 299 "multiple recipients are accepted"); 300 assertThrows(IllegalArgumentException.class, 301 () -> AcmeUtils.validateContact(URI.create("mailto:foo@example.com?to=bar@example.com")), 302 "hfields are accepted"); 303 assertThrows(IllegalArgumentException.class, 304 () -> AcmeUtils.validateContact(URI.create("mailto:?to=foo@example.com")), 305 "only hfields are accepted"); 306 } 307 308}