mirror of https://github.com/shred/acme4j
Move JOSE related methods into an utility class
parent
d6b53b0bbd
commit
a1db2fa29b
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue