From 9d3ab4972cc70a882e3b862c32ca9c957d188538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sat, 12 Jan 2019 16:28:07 +0100 Subject: [PATCH] Validate challenge tokens before use --- .../acme4j/challenge/TokenChallenge.java | 7 ++- .../acme4j/connector/DefaultConnection.java | 4 +- .../shredzone/acme4j/toolbox/AcmeUtils.java | 16 ++++++ .../acme4j/challenge/TokenChallengeTest.java | 56 +++++++++++++++++++ .../acme4j/toolbox/AcmeUtilsTest.java | 16 ++++++ 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TokenChallengeTest.java 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(".illegal#Text"), is(false)); + } + /** * Test ACE conversion. */