Move JOSE related methods into an utility class

pull/81/head
Richard Körber 2019-03-24 13:34:41 +01:00
parent d6b53b0bbd
commit a1db2fa29b
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
10 changed files with 591 additions and 303 deletions

View File

@ -14,7 +14,6 @@
package org.shredzone.acme4j;
import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.keyAlgorithm;
import java.net.URI;
import java.net.URL;
@ -29,9 +28,6 @@ import java.util.Objects;
import javax.annotation.CheckForNull;
import javax.annotation.ParametersAreNonnullByDefault;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.connector.ResourceIterator;
@ -42,6 +38,7 @@ import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.shredzone.acme4j.toolbox.JoseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -209,30 +206,17 @@ public class Account extends AcmeJsonResource {
try (Connection conn = getSession().connect()) {
URL keyChangeUrl = getSession().resourceUrl(Resource.KEY_CHANGE);
PublicJsonWebKey newKeyJwk = PublicJsonWebKey.Factory.newPublicJwk(newKeyPair.getPublic());
JSONBuilder payloadClaim = new JSONBuilder();
payloadClaim.put("account", getLocation());
payloadClaim.putKey("oldKey", getLogin().getKeyPair().getPublic());
JsonWebSignature innerJws = new JsonWebSignature();
innerJws.setPayload(payloadClaim.toString());
innerJws.getHeaders().setObjectHeaderValue("url", keyChangeUrl);
innerJws.getHeaders().setJwkHeaderValue("jwk", newKeyJwk);
innerJws.setAlgorithmHeaderValue(keyAlgorithm(newKeyJwk));
innerJws.setKey(newKeyPair.getPrivate());
innerJws.sign();
JSONBuilder jose = JoseUtils.createJoseRequest(keyChangeUrl, newKeyPair,
payloadClaim, null, null);
JSONBuilder outerClaim = new JSONBuilder();
outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
outerClaim.put("signature", innerJws.getEncodedSignature());
outerClaim.put("payload", innerJws.getEncodedPayload());
conn.sendSignedRequest(keyChangeUrl, outerClaim, getLogin());
conn.sendSignedRequest(keyChangeUrl, jose, getLogin());
getLogin().setKeyPair(newKeyPair);
} catch (JoseException ex) {
throw new AcmeProtocolException("Cannot sign key-change", ex);
}
}

View File

@ -14,23 +14,17 @@
package org.shredzone.acme4j;
import static java.util.Objects.requireNonNull;
import static org.shredzone.acme4j.toolbox.AcmeUtils.macKeyAlgorithm;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.keys.HmacKey;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
@ -38,6 +32,7 @@ import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.shredzone.acme4j.toolbox.JoseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -166,7 +161,7 @@ public class AccountBuilder {
*/
public AccountBuilder withKeyIdentifier(String kid, String encodedMacKey) {
byte[] encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey"));
return withKeyIdentifier(kid, new HmacKey(encodedKey));
return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC"));
}
/**
@ -209,7 +204,7 @@ public class AccountBuilder {
claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
}
if (keyIdentifier != null) {
claims.put("externalAccountBinding", createExternalAccountBinding(
claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
keyIdentifier, keyPair.getPublic(), macKey, resourceUrl));
}
if (onlyExisting != null) {
@ -232,42 +227,4 @@ public class AccountBuilder {
}
}
/**
* Creates a JSON structure for external account binding.
*
* @param kid
* Key Identifier provided by the CA
* @param accountKey
* {@link PublicKey} of the account to register
* @param macKey
* {@link SecretKey} to sign the key identifier with
* @param resource
* "newAccount" resource URL
* @return Created JSON structure
*/
private Map<String, Object> createExternalAccountBinding(String kid,
PublicKey accountKey, SecretKey macKey, URL resource)
throws AcmeException {
try {
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
JsonWebSignature innerJws = new JsonWebSignature();
innerJws.setPayload(keyJwk.toJson());
innerJws.getHeaders().setObjectHeaderValue("url", resource);
innerJws.getHeaders().setObjectHeaderValue("kid", kid);
innerJws.setAlgorithmHeaderValue(macKeyAlgorithm(macKey));
innerJws.setKey(macKey);
innerJws.setDoKeyValidation(false);
innerJws.sign();
JSONBuilder outerClaim = new JSONBuilder();
outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
outerClaim.put("signature", innerJws.getEncodedSignature());
outerClaim.put("payload", innerJws.getEncodedPayload());
return outerClaim.toMap();
} catch (JoseException ex) {
throw new AcmeException("Could not create external account binding", ex);
}
}
}

View File

@ -19,12 +19,11 @@ import java.security.PublicKey;
import javax.annotation.ParametersAreNonnullByDefault;
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;
import org.shredzone.acme4j.toolbox.JoseUtils;
/**
* An extension of {@link Challenge} that handles challenges with a {@code token} and
@ -66,15 +65,8 @@ public class TokenChallenge extends Challenge {
* override this method if a different algorithm is used.
*/
public String getAuthorization() {
try {
PublicKey pk = getLogin().getKeyPair().getPublic();
PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(pk);
return getToken()
+ '.'
+ base64UrlEncode(jwk.calculateThumbprint("SHA-256"));
} catch (JoseException ex) {
throw new AcmeProtocolException("Cannot compute key thumbprint", ex);
}
PublicKey pk = getLogin().getKeyPair().getPublic();
return getToken() + '.' + base64UrlEncode(JoseUtils.thumbprint(pk));
}
}

View File

@ -14,7 +14,6 @@
package org.shredzone.acme4j.connector;
import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.keyAlgorithm;
import java.io.IOException;
import java.io.InputStream;
@ -24,6 +23,7 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@ -41,9 +41,6 @@ import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session;
@ -58,6 +55,7 @@ import org.shredzone.acme4j.exception.AcmeUserActionRequiredException;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.shredzone.acme4j.toolbox.JoseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -385,8 +383,6 @@ public class DefaultConnection implements Connection {
resetNonce(session);
}
String claimJson = claims != null ? claims.toString() : "";
conn = httpConnector.openConnection(url, session.getProxy());
conn.setRequestMethod("POST");
conn.setRequestProperty(ACCEPT_HEADER, accept);
@ -395,34 +391,15 @@ public class DefaultConnection implements Connection {
conn.setRequestProperty(CONTENT_TYPE_HEADER, "application/jose+json");
conn.setDoOutput(true);
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claimJson);
jws.getHeaders().setObjectHeaderValue("nonce", session.getNonce());
jws.getHeaders().setObjectHeaderValue("url", url);
if (accountLocation == null) {
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
} else {
jws.getHeaders().setObjectHeaderValue("kid", accountLocation);
}
JSONBuilder jose = JoseUtils.createJoseRequest(
url,
keypair,
claims,
session.getNonce(),
accountLocation != null ? accountLocation.toString() : null
);
jws.setAlgorithmHeaderValue(keyAlgorithm(jwk));
jws.setKey(keypair.getPrivate());
jws.sign();
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", claims != null ? "POST" : "POST-as-GET", url);
if (claims != null) {
LOG.debug(" Payload: {}", claimJson);
}
LOG.debug(" JWS Header: {}", jws.getHeaders().getFullHeaderAsJsonString());
}
JSONBuilder jb = new JSONBuilder();
jb.put("protected", jws.getHeaders().getEncodedHeader());
jb.put("payload", jws.getEncodedPayload());
jb.put("signature", jws.getEncodedSignature());
byte[] outputData = jb.toString().getBytes(DEFAULT_CHARSET);
byte[] outputData = jose.toString().getBytes(StandardCharsets.UTF_8);
conn.setFixedLengthStreamingMode(outputData.length);
conn.connect();
@ -442,8 +419,6 @@ public class DefaultConnection implements Connection {
return rc;
} catch (IOException ex) {
throw new AcmeNetworkException(ex);
} catch (JoseException ex) {
throw new AcmeProtocolException("Failed to generate a JSON request", ex);
}
}

View File

@ -34,13 +34,7 @@ import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.WillNotClose;
import javax.annotation.concurrent.Immutable;
import javax.crypto.SecretKey;
import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.shredzone.acme4j.exception.AcmeProtocolException;
/**
@ -191,74 +185,6 @@ public final class AcmeUtils {
return IDN.toASCII(domain.trim()).toLowerCase();
}
/**
* Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm
* identifier for {@link JsonWebSignature}.
*
* @param jwk
* {@link JsonWebKey} to analyze
* @return algorithm identifier
* @throws IllegalArgumentException
* there is no corresponding algorithm identifier for the key
*/
public static String keyAlgorithm(JsonWebKey jwk) {
if (jwk instanceof EllipticCurveJsonWebKey) {
EllipticCurveJsonWebKey ecjwk = (EllipticCurveJsonWebKey) jwk;
switch (ecjwk.getCurveName()) {
case "P-256":
return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256;
case "P-384":
return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384;
case "P-521":
return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512;
default:
throw new IllegalArgumentException("Unknown EC name "
+ ecjwk.getCurveName());
}
} else if (jwk instanceof RsaJsonWebKey) {
return AlgorithmIdentifiers.RSA_USING_SHA256;
} else {
throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm());
}
}
/**
* Analyzes the {@link SecretKey}, and returns the key algorithm
* identifier for {@link JsonWebSignature}.
*
* @param macKey
* {@link SecretKey} to analyze
* @return algorithm identifier
* @throws IllegalArgumentException
* there is no corresponding algorithm identifier for the key
*/
public static String macKeyAlgorithm(SecretKey macKey) {
if (!"HMAC".equals(macKey.getAlgorithm())) {
throw new IllegalArgumentException("Bad algorithm: " + macKey.getAlgorithm());
}
int size = macKey.getEncoded().length * 8;
switch (size) {
case 256:
return AlgorithmIdentifiers.HMAC_SHA256;
case 384:
return AlgorithmIdentifiers.HMAC_SHA384;
case 512:
return AlgorithmIdentifiers.HMAC_SHA512;
default:
throw new IllegalArgumentException("Bad key size: " + size);
}
}
/**
* Parses a RFC 3339 formatted date.
*

View File

@ -30,10 +30,6 @@ import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
/**
* Builder for JSON structures.
@ -131,14 +127,8 @@ public class JSONBuilder {
public JSONBuilder putKey(String key, PublicKey publickey) {
Objects.requireNonNull(publickey, "publickey");
try {
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(publickey);
Map<String, Object> jwkParams = jwk.toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
object(key).data.putAll(jwkParams);
return this;
} catch (JoseException ex) {
throw new AcmeProtocolException("Invalid key", ex);
}
data.put(key, JoseUtils.publicKeyToJWK(publickey));
return this;
}
/**

View File

@ -0,0 +1,247 @@
/*
* 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.toolbox;
import java.net.URL;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Map;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.crypto.SecretKey;
import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class that takes care of all the JOSE stuff.
*
* @since 2.7
*/
@ParametersAreNonnullByDefault
public final class JoseUtils {
private static final Logger LOG = LoggerFactory.getLogger(JoseUtils.class);
private JoseUtils() {
// Utility class without constructor
}
/**
* Creates an ACME JOSE request.
*
* @param url
* {@link URL} of the ACME call
* @param keypair
* {@link KeyPair} to sign the request with
* @param payload
* ACME JSON payload. If {@code null}, a POST-as-GET request is generated
* instead.
* @param nonce
* Nonce to be used. {@code null} if no nonce is to be used in the JOSE
* header.
* @param kid
* kid to be used in the JOSE header. If {@code null}, a jwk header of the
* given key is used instead.
* @return JSON structure of the JOSE request, ready to be sent.
*/
public static JSONBuilder createJoseRequest(URL url, KeyPair keypair,
@Nullable JSONBuilder payload, @Nullable String nonce, @Nullable String kid) {
try {
PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
JsonWebSignature jws = new JsonWebSignature();
jws.getHeaders().setObjectHeaderValue("url", url);
if (kid != null) {
jws.getHeaders().setObjectHeaderValue("kid", kid);
} else {
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
}
if (nonce != null) {
jws.getHeaders().setObjectHeaderValue("nonce", nonce);
}
jws.setPayload(payload != null ? payload.toString() : "");
jws.setAlgorithmHeaderValue(keyAlgorithm(jwk));
jws.setKey(keypair.getPrivate());
jws.sign();
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", payload != null ? "POST" : "POST-as-GET", url);
if (payload != null) {
LOG.debug(" Payload: {}", payload);
}
LOG.debug(" JWS Header: {}", jws.getHeaders().getFullHeaderAsJsonString());
}
JSONBuilder jb = new JSONBuilder();
jb.put("protected", jws.getHeaders().getEncodedHeader());
jb.put("payload", jws.getEncodedPayload());
jb.put("signature", jws.getEncodedSignature());
return jb;
} catch (JoseException ex) {
throw new AcmeProtocolException("Failed to sign a JSON request", ex);
}
}
/**
* Creates a JSON structure for external account binding.
*
* @param kid
* Key Identifier provided by the CA
* @param accountKey
* {@link PublicKey} of the account to register
* @param macKey
* {@link SecretKey} to sign the key identifier with
* @param resource
* "newAccount" resource URL
* @return Created JSON structure
*/
public static Map<String, Object> createExternalAccountBinding(String kid,
PublicKey accountKey, SecretKey macKey, URL resource) throws AcmeException {
try {
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
JsonWebSignature innerJws = new JsonWebSignature();
innerJws.setPayload(keyJwk.toJson());
innerJws.getHeaders().setObjectHeaderValue("url", resource);
innerJws.getHeaders().setObjectHeaderValue("kid", kid);
innerJws.setAlgorithmHeaderValue(macKeyAlgorithm(macKey));
innerJws.setKey(macKey);
innerJws.setDoKeyValidation(false);
innerJws.sign();
JSONBuilder outerClaim = new JSONBuilder();
outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
outerClaim.put("signature", innerJws.getEncodedSignature());
outerClaim.put("payload", innerJws.getEncodedPayload());
return outerClaim.toMap();
} catch (JoseException ex) {
throw new AcmeException("Could not create external account binding", ex);
}
}
/**
* Converts a {@link PublicKey} to a JOSE JWK structure.
*
* @param key
* {@link PublicKey} to convert
* @return JSON map containing the JWK structure
*/
public static Map<String, Object> publicKeyToJWK(PublicKey key) {
try {
return PublicJsonWebKey.Factory.newPublicJwk(key)
.toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
} catch (JoseException ex) {
throw new IllegalArgumentException("Bad public key", ex);
}
}
/**
* Computes a thumbprint of the given public key.
*
* @param key
* {@link PublicKey} to get the thumbprint of
* @return Thumbprint of the key
*/
public static byte[] thumbprint(PublicKey key) {
try {
PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(key);
return jwk.calculateThumbprint("SHA-256");
} catch (JoseException ex) {
throw new IllegalArgumentException("Bad public key", ex);
}
}
/**
* Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm
* identifier for {@link JsonWebSignature}.
*
* @param jwk
* {@link JsonWebKey} to analyze
* @return algorithm identifier
* @throws IllegalArgumentException
* there is no corresponding algorithm identifier for the key
*/
public static String keyAlgorithm(JsonWebKey jwk) {
if (jwk instanceof EllipticCurveJsonWebKey) {
EllipticCurveJsonWebKey ecjwk = (EllipticCurveJsonWebKey) jwk;
switch (ecjwk.getCurveName()) {
case "P-256":
return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256;
case "P-384":
return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384;
case "P-521":
return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512;
default:
throw new IllegalArgumentException("Unknown EC name "
+ ecjwk.getCurveName());
}
} else if (jwk instanceof RsaJsonWebKey) {
return AlgorithmIdentifiers.RSA_USING_SHA256;
} else {
throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm());
}
}
/**
* Analyzes the {@link SecretKey}, and returns the key algorithm identifier for {@link
* JsonWebSignature}.
*
* @param macKey
* {@link SecretKey} to analyze
* @return algorithm identifier
* @throws IllegalArgumentException
* there is no corresponding algorithm identifier for the key
*/
public static String macKeyAlgorithm(SecretKey macKey) {
if (!"HMAC".equals(macKey.getAlgorithm())) {
throw new IllegalArgumentException("Bad algorithm: " + macKey.getAlgorithm());
}
int size = macKey.getEncoded().length * 8;
switch (size) {
case 256:
return AlgorithmIdentifiers.HMAC_SHA256;
case 384:
return AlgorithmIdentifiers.HMAC_SHA384;
case 512:
return AlgorithmIdentifiers.HMAC_SHA512;
default:
throw new IllegalArgumentException("Bad key size: " + size);
}
}
}

View File

@ -13,9 +13,11 @@
*/
package org.shredzone.acme4j;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.shredzone.acme4j.toolbox.TestUtils.*;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
import static org.shredzone.acme4j.toolbox.TestUtils.url;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.net.HttpURLConnection;
@ -24,9 +26,7 @@ import java.security.KeyPair;
import javax.crypto.SecretKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwx.CompactSerializer;
import org.jose4j.lang.JoseException;
import org.junit.Test;
import org.mockito.Mockito;
import org.shredzone.acme4j.connector.Resource;
@ -34,6 +34,7 @@ import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.shredzone.acme4j.toolbox.JoseUtilsTest;
import org.shredzone.acme4j.toolbox.TestUtils;
/**
@ -115,41 +116,20 @@ public class AccountBuilderTest {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair) {
try {
assertThat(session, is(notNullValue()));
assertThat(url, is(resourceUrl));
assertThat(keypair, is(accountKey));
assertThat(session, is(notNullValue()));
assertThat(url, is(resourceUrl));
assertThat(keypair, is(accountKey));
JSON binding = claims.toJSON()
.get("externalAccountBinding")
.asObject();
JSON binding = claims.toJSON()
.get("externalAccountBinding")
.asObject();
String encodedHeader = binding.get("protected").asString();
String encodedSignature = binding.get("signature").asString();
String encodedPayload = binding.get("payload").asString();
String encodedHeader = binding.get("protected").asString();
String encodedSignature = binding.get("signature").asString();
String encodedPayload = binding.get("payload").asString();
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(serialized);
jws.setKey(macKey);
assertThat(jws.verifySignature(), is(true));
assertThat(jws.getHeader("url"), is(resourceUrl.toString()));
assertThat(jws.getHeader("kid"), is(keyIdentifier));
assertThat(jws.getHeader("alg"), is("HS256"));
String decodedPayload = jws.getPayload();
StringBuilder expectedPayload = new StringBuilder();
expectedPayload.append('{');
expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\",");
expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\",");
expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\"");
expectedPayload.append("}");
assertThat(decodedPayload, sameJSONAs(expectedPayload.toString()));
} catch (JoseException ex) {
ex.printStackTrace();
fail("decoding inner payload failed");
}
JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey);
return HttpURLConnection.HTTP_CREATED;
}

View File

@ -13,8 +13,10 @@
*/
package org.shredzone.acme4j.toolbox;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.shredzone.acme4j.toolbox.AcmeUtils.*;
import java.io.ByteArrayOutputStream;
@ -25,7 +27,6 @@ import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
@ -38,11 +39,10 @@ import java.util.List;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.jose4j.jwk.PublicJsonWebKey;
import org.junit.BeforeClass;
import org.junit.Test;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.AcmeUtils.PemLabel;
import org.shredzone.acme4j.toolbox.AcmeUtils.*;
/**
* Unit tests for {@link AcmeUtils}.
@ -133,68 +133,6 @@ public class AcmeUtilsTest {
is("xn--exmle-hra7p.xn--m-7ba6w"));
}
/**
* Test if RSA using SHA-256 keys are properly detected.
*/
@Test
public void testRsaKey() throws Exception {
KeyPair rsaKeyPair = TestUtils.createKeyPair();
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(rsaKeyPair.getPublic());
String type = keyAlgorithm(jwk);
assertThat(type, is("RS256"));
}
/**
* Test if ECDSA using NIST P-256 curve and SHA-256 keys are properly detected.
*/
@Test
public void testP256ECKey() throws Exception {
KeyPair ecKeyPair = TestUtils.createECKeyPair("secp256r1");
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());
String type = keyAlgorithm(jwk);
assertThat(type, is("ES256"));
}
/**
* Test if ECDSA using NIST P-384 curve and SHA-384 keys are properly detected.
*/
@Test
public void testP384ECKey() throws Exception {
KeyPair ecKeyPair = TestUtils.createECKeyPair("secp384r1");
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());
String type = keyAlgorithm(jwk);
assertThat(type, is("ES384"));
}
/**
* Test if ECDSA using NIST P-521 curve and SHA-512 keys are properly detected.
*/
@Test
public void testP521ECKey() throws Exception {
KeyPair ecKeyPair = TestUtils.createECKeyPair("secp521r1");
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());
String type = keyAlgorithm(jwk);
assertThat(type, is("ES512"));
}
/**
* Test if MAC key algorithms are properly detected.
*/
@Test
public void testMacKey() throws Exception {
assertThat(macKeyAlgorithm(TestUtils.createSecretKey("SHA-256")), is("HS256"));
assertThat(macKeyAlgorithm(TestUtils.createSecretKey("SHA-384")), is("HS384"));
assertThat(macKeyAlgorithm(TestUtils.createSecretKey("SHA-512")), is("HS512"));
}
/**
* Test valid strings.
*/

View File

@ -0,0 +1,299 @@
/*
* 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.toolbox;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.shredzone.acme4j.toolbox.TestUtils.url;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.net.URL;
import java.security.KeyPair;
import java.util.Base64;
import java.util.Map;
import javax.crypto.SecretKey;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwx.CompactSerializer;
import org.jose4j.lang.JoseException;
import org.junit.Test;
/**
* Unit tests for {@link JoseUtils}.
*/
public class JoseUtilsTest {
private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();
/**
* Test if a JOSE ACME POST request is correctly created.
*/
@Test
public void testCreateJosePostRequest() throws Exception {
URL resourceUrl = url("http://example.com/acme/resource");
KeyPair accountKey = TestUtils.createKeyPair();
String nonce = URL_ENCODER.encodeToString("foo-nonce-1-foo".getBytes());
JSONBuilder payload = new JSONBuilder();
payload.put("foo", 123);
payload.put("bar", "a-string");
Map<String, Object> jose = JoseUtils
.createJoseRequest(resourceUrl, accountKey, payload, nonce, TestUtils.ACCOUNT_URL)
.toMap();
String encodedHeader = jose.get("protected").toString();
String encodedSignature = jose.get("signature").toString();
String encodedPayload = jose.get("payload").toString();
StringBuilder expectedHeader = new StringBuilder();
expectedHeader.append('{');
expectedHeader.append("\"nonce\":\"").append(nonce).append("\",");
expectedHeader.append("\"url\":\"").append(resourceUrl).append("\",");
expectedHeader.append("\"alg\":\"RS256\",");
expectedHeader.append("\"kid\":\"").append(TestUtils.ACCOUNT_URL).append('"');
expectedHeader.append('}');
assertThat(new String(URL_DECODER.decode(encodedHeader), UTF_8), sameJSONAs(expectedHeader.toString()));
assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8), sameJSONAs("{\"foo\":123,\"bar\":\"a-string\"}"));
assertThat(encodedSignature, not(emptyOrNullString()));
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));
jws.setKey(accountKey.getPublic());
assertThat(jws.verifySignature(), is(true));
}
/**
* Test if a JOSE ACME POST-as-GET request is correctly created.
*/
@Test
public void testCreateJosePostAsGetRequest() throws Exception {
URL resourceUrl = url("http://example.com/acme/resource");
KeyPair accountKey = TestUtils.createKeyPair();
String nonce = URL_ENCODER.encodeToString("foo-nonce-1-foo".getBytes());
Map<String, Object> jose = JoseUtils
.createJoseRequest(resourceUrl, accountKey, null, nonce, TestUtils.ACCOUNT_URL)
.toMap();
String encodedHeader = jose.get("protected").toString();
String encodedSignature = jose.get("signature").toString();
String encodedPayload = jose.get("payload").toString();
StringBuilder expectedHeader = new StringBuilder();
expectedHeader.append('{');
expectedHeader.append("\"nonce\":\"").append(nonce).append("\",");
expectedHeader.append("\"url\":\"").append(resourceUrl).append("\",");
expectedHeader.append("\"alg\":\"RS256\",");
expectedHeader.append("\"kid\":\"").append(TestUtils.ACCOUNT_URL).append('"');
expectedHeader.append('}');
assertThat(new String(URL_DECODER.decode(encodedHeader), UTF_8), sameJSONAs(expectedHeader.toString()));
assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8), is(""));
assertThat(encodedSignature, not(emptyOrNullString()));
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));
jws.setKey(accountKey.getPublic());
assertThat(jws.verifySignature(), is(true));
}
/**
* Test if a JOSE ACME Key-Change request is correctly created.
*/
@Test
public void testCreateJoseKeyChangeRequest() throws Exception {
URL resourceUrl = url("http://example.com/acme/resource");
KeyPair accountKey = TestUtils.createKeyPair();
JSONBuilder payload = new JSONBuilder();
payload.put("foo", 123);
payload.put("bar", "a-string");
Map<String, Object> jose = JoseUtils
.createJoseRequest(resourceUrl, accountKey, payload, null, null)
.toMap();
String encodedHeader = jose.get("protected").toString();
String encodedSignature = jose.get("signature").toString();
String encodedPayload = jose.get("payload").toString();
StringBuilder expectedHeader = new StringBuilder();
expectedHeader.append('{');
expectedHeader.append("\"url\":\"").append(resourceUrl).append("\",");
expectedHeader.append("\"alg\":\"RS256\",");
expectedHeader.append("\"jwk\": {");
expectedHeader.append("\"kty\": \"").append(TestUtils.KTY).append("\",");
expectedHeader.append("\"e\": \"").append(TestUtils.E).append("\",");
expectedHeader.append("\"n\": \"").append(TestUtils.N).append("\"}");
expectedHeader.append("}");
assertThat(new String(URL_DECODER.decode(encodedHeader), UTF_8), sameJSONAs(expectedHeader.toString()));
assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8), sameJSONAs("{\"foo\":123,\"bar\":\"a-string\"}"));
assertThat(encodedSignature, not(emptyOrNullString()));
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature));
jws.setKey(accountKey.getPublic());
assertThat(jws.verifySignature(), is(true));
}
/**
* Test if an external account binding is correctly created.
*/
@Test
public void testCreateExternalAccountBinding() throws Exception {
KeyPair accountKey = TestUtils.createKeyPair();
String keyIdentifier = "NCC-1701";
SecretKey macKey = TestUtils.createSecretKey("SHA-256");
URL resourceUrl = url("http://example.com/acme/resource");
Map<String, Object> binding = JoseUtils.createExternalAccountBinding(
keyIdentifier, accountKey.getPublic(), macKey, resourceUrl);
String encodedHeader = binding.get("protected").toString();
String encodedSignature = binding.get("signature").toString();
String encodedPayload = binding.get("payload").toString();
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey);
}
/**
* Test if public key is correctly converted to JWK structure.
*/
@Test
public void testPublicKeyToJWK() throws Exception {
Map<String, Object> json = JoseUtils.publicKeyToJWK(TestUtils.createKeyPair().getPublic());
assertThat(json.size(), is(3));
assertThat(json.get("kty"), is(TestUtils.KTY));
assertThat(json.get("n"), is(TestUtils.N));
assertThat(json.get("e"), is(TestUtils.E));
}
/**
* Test if thumbprint is correctly computed.
*/
@Test
public void testThumbprint() throws Exception {
byte[] thumb = JoseUtils.thumbprint(TestUtils.createKeyPair().getPublic());
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(thumb);
assertThat(encoded, is(TestUtils.THUMBPRINT));
}
/**
* Test if RSA using SHA-256 keys are properly detected.
*/
@Test
public void testRsaKey() throws Exception {
KeyPair rsaKeyPair = TestUtils.createKeyPair();
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(rsaKeyPair.getPublic());
String type = JoseUtils.keyAlgorithm(jwk);
assertThat(type, is("RS256"));
}
/**
* Test if ECDSA using NIST P-256 curve and SHA-256 keys are properly detected.
*/
@Test
public void testP256ECKey() throws Exception {
KeyPair ecKeyPair = TestUtils.createECKeyPair("secp256r1");
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());
String type = JoseUtils.keyAlgorithm(jwk);
assertThat(type, is("ES256"));
}
/**
* Test if ECDSA using NIST P-384 curve and SHA-384 keys are properly detected.
*/
@Test
public void testP384ECKey() throws Exception {
KeyPair ecKeyPair = TestUtils.createECKeyPair("secp384r1");
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());
String type = JoseUtils.keyAlgorithm(jwk);
assertThat(type, is("ES384"));
}
/**
* Test if ECDSA using NIST P-521 curve and SHA-512 keys are properly detected.
*/
@Test
public void testP521ECKey() throws Exception {
KeyPair ecKeyPair = TestUtils.createECKeyPair("secp521r1");
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic());
String type = JoseUtils.keyAlgorithm(jwk);
assertThat(type, is("ES512"));
}
/**
* Test if MAC key algorithms are properly detected.
*/
@Test
public void testMacKey() throws Exception {
assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey("SHA-256")), is("HS256"));
assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey("SHA-384")), is("HS384"));
assertThat(JoseUtils.macKeyAlgorithm(TestUtils.createSecretKey("SHA-512")), is("HS512"));
}
/**
* Asserts that the serialized external account binding is valid. Unit test fails if
* the account binding is invalid.
*
* @param serialized
* Serialized external account binding JOSE structure
* @param resourceUrl
* Expected resource {@link URL}
* @param keyIdentifier
* Expected key identifier
* @param macKey
* Expected {@link SecretKey}
*/
public static void assertExternalAccountBinding(String serialized, URL resourceUrl,
String keyIdentifier, SecretKey macKey) {
try {
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(serialized);
jws.setKey(macKey);
assertThat(jws.verifySignature(), is(true));
assertThat(jws.getHeader("url"), is(resourceUrl.toString()));
assertThat(jws.getHeader("kid"), is(keyIdentifier));
assertThat(jws.getHeader("alg"), is("HS256"));
String decodedPayload = jws.getPayload();
StringBuilder expectedPayload = new StringBuilder();
expectedPayload.append('{');
expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\",");
expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\",");
expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\"");
expectedPayload.append("}");
assertThat(decodedPayload, sameJSONAs(expectedPayload.toString()));
} catch (JoseException ex) {
fail(ex.getMessage());
}
}
}