diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java index d5d63c9c..f47ae091 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java @@ -23,6 +23,7 @@ import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.lang.JoseException; import org.shredzone.acme4j.Login; import org.shredzone.acme4j.exception.AcmeProtocolException; +import org.shredzone.acme4j.toolbox.AcmeUtils; import org.shredzone.acme4j.toolbox.JSON; /** @@ -51,7 +52,11 @@ public class TokenChallenge extends Challenge { * Gets the token. */ protected String getToken() { - return getJSON().get(KEY_TOKEN).asString(); + String token = getJSON().get(KEY_TOKEN).asString(); + if (!AcmeUtils.isValidBase64Url(token)) { + throw new AcmeProtocolException("Invalid token: " + token); + } + return token; } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index c28b70ce..24307f39 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -82,8 +82,6 @@ public class DefaultConnection implements Connection { private static final String MIME_JSON_PROBLEM = "application/problem+json"; private static final String MIME_CERTIFICATE_CHAIN = "application/pem-certificate-chain"; - private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+"); - private static final URI BAD_NONCE_ERROR = URI.create("urn:ietf:params:acme:error:badNonce"); private static final int MAX_ATTEMPTS = 10; @@ -235,7 +233,7 @@ public class DefaultConnection implements Connection { return null; } - if (!BASE64URL_PATTERN.matcher(nonceHeader).matches()) { + if (!AcmeUtils.isValidBase64Url(nonceHeader)) { throw new AcmeProtocolException("Invalid replay nonce: " + nonceHeader); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java index 368eb669..3a9c81b9 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java @@ -69,6 +69,8 @@ public final class AcmeUtils { private static final Pattern MAIL_PATTERN = Pattern.compile("\\?|@.*,"); + private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]*"); + private static final Base64.Encoder PEM_ENCODER = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)); @@ -156,6 +158,20 @@ public final class AcmeUtils { return Base64Url.decode(base64); } + /** + * Validates that the given {@link String} is a valid base64url encoded value. + * + * @param base64 + * {@link String} to validate + * @return {@code true}: String contains a valid base64url encoded value. + * {@code false} if the {@link String} was {@code null} or contained illegal + * characters. + * @since 2.6 + */ + public static boolean isValidBase64Url(@Nullable String base64) { + return base64 != null && BASE64URL_PATTERN.matcher(base64).matches(); + } + /** * ASCII encodes a domain name. *
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TokenChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TokenChallengeTest.java
new file mode 100644
index 00000000..1aedc93f
--- /dev/null
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TokenChallengeTest.java
@@ -0,0 +1,56 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2019 Richard "Shred" Körber
+ * http://acme4j.shredzone.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+package org.shredzone.acme4j.challenge;
+
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+import org.junit.Test;
+import org.shredzone.acme4j.Login;
+import org.shredzone.acme4j.exception.AcmeProtocolException;
+import org.shredzone.acme4j.provider.TestableConnectionProvider;
+import org.shredzone.acme4j.toolbox.JSONBuilder;
+
+/**
+ * Unit tests for {@link TokenChallenge}.
+ */
+public class TokenChallengeTest {
+
+ /**
+ * Test that invalid tokens are detected.
+ */
+ @Test
+ public void testInvalidToken() throws IOException {
+ TestableConnectionProvider provider = new TestableConnectionProvider();
+ Login login = provider.createLogin();
+
+ JSONBuilder jb = new JSONBuilder();
+ jb.put("url", "https://example.com/acme/1234");
+ jb.put("type", "generic");
+ jb.put("token", "");
+
+ TokenChallenge challenge = new TokenChallenge(login, jb.toJSON());
+
+ try {
+ challenge.getToken();
+ fail("Invalid token was accepted");
+ } catch (AcmeProtocolException ex) {
+ // expected
+ }
+
+ provider.close();
+ }
+
+}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java
index 21271b3c..eac94010 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java
@@ -92,6 +92,22 @@ public class AcmeUtilsTest {
assertThat(base64UrlDecode, is(sha256hash("foobar")));
}
+ /**
+ * Test base64 URL validation.
+ */
+ @Test
+ public void testBase64UrlValidate() {
+ assertThat(isValidBase64Url(null), is(false));
+ assertThat(isValidBase64Url(""), is(true));
+ assertThat(isValidBase64Url(" "), is(false));
+ assertThat(isValidBase64Url("Zg"), is(true));
+ assertThat(isValidBase64Url("Zg="), is(false));
+ assertThat(isValidBase64Url("Zg=="), is(false));
+ assertThat(isValidBase64Url("Zm9v"), is(true));
+ assertThat(isValidBase64Url(" Zm9v "), is(false));
+ assertThat(isValidBase64Url("