001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2019 Richard "Shred" K├Ârber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j.toolbox;
015
016import java.net.URL;
017import java.security.KeyPair;
018import java.security.PublicKey;
019import java.util.Map;
020
021import javax.crypto.SecretKey;
022
023import edu.umd.cs.findbugs.annotations.Nullable;
024import org.jose4j.jwk.EllipticCurveJsonWebKey;
025import org.jose4j.jwk.JsonWebKey;
026import org.jose4j.jwk.PublicJsonWebKey;
027import org.jose4j.jwk.RsaJsonWebKey;
028import org.jose4j.jws.AlgorithmIdentifiers;
029import org.jose4j.jws.JsonWebSignature;
030import org.jose4j.lang.JoseException;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Utility class that takes care of all the JOSE stuff.
036 * <p>
037 * Internal class, do not use in your project! The API may change anytime, in a breaking
038 * manner, and without prior notice.
039 *
040 * @since 2.7
041 */
042public final class JoseUtils {
043
044    private static final Logger LOG = LoggerFactory.getLogger(JoseUtils.class);
045
046    private JoseUtils() {
047        // Utility class without constructor
048    }
049
050    /**
051     * Creates an ACME JOSE request.
052     *
053     * @param url
054     *         {@link URL} of the ACME call
055     * @param keypair
056     *         {@link KeyPair} to sign the request with
057     * @param payload
058     *         ACME JSON payload. If {@code null}, a POST-as-GET request is generated
059     *         instead.
060     * @param nonce
061     *         Nonce to be used. {@code null} if no nonce is to be used in the JOSE
062     *         header.
063     * @param kid
064     *         kid to be used in the JOSE header. If {@code null}, a jwk header of the
065     *         given key is used instead.
066     * @return JSON structure of the JOSE request, ready to be sent.
067     */
068    public static JSONBuilder createJoseRequest(URL url, KeyPair keypair,
069                @Nullable JSONBuilder payload, @Nullable String nonce, @Nullable String kid) {
070        try {
071            var jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
072
073            var jws = new JsonWebSignature();
074            jws.getHeaders().setObjectHeaderValue("url", url);
075
076            if (kid != null) {
077                jws.getHeaders().setObjectHeaderValue("kid", kid);
078            } else {
079                jws.getHeaders().setJwkHeaderValue("jwk", jwk);
080            }
081
082            if (nonce != null) {
083                jws.getHeaders().setObjectHeaderValue("nonce", nonce);
084            }
085
086            jws.setPayload(payload != null ? payload.toString() : "");
087            jws.setAlgorithmHeaderValue(keyAlgorithm(jwk));
088            jws.setKey(keypair.getPrivate());
089            jws.sign();
090
091            if (LOG.isDebugEnabled()) {
092                LOG.debug("{} {}", payload != null ? "POST" : "POST-as-GET", url);
093                if (payload != null) {
094                    LOG.debug("  Payload: {}", payload);
095                }
096                LOG.debug("  JWS Header: {}", jws.getHeaders().getFullHeaderAsJsonString());
097            }
098
099            var jb = new JSONBuilder();
100            jb.put("protected", jws.getHeaders().getEncodedHeader());
101            jb.put("payload", jws.getEncodedPayload());
102            jb.put("signature", jws.getEncodedSignature());
103            return jb;
104        } catch (JoseException ex) {
105            throw new IllegalArgumentException("Could not create a JOSE request", ex);
106        }
107    }
108
109    /**
110     * Creates a JSON structure for external account binding.
111     *
112     * @param kid
113     *         Key Identifier provided by the CA
114     * @param accountKey
115     *         {@link PublicKey} of the account to register
116     * @param macKey
117     *         {@link SecretKey} to sign the key identifier with
118     * @param macAlgorithm
119     *         Algorithm of the MAC key
120     * @param resource
121     *         "newAccount" resource URL
122     * @return Created JSON structure
123     */
124    public static Map<String, Object> createExternalAccountBinding(String kid,
125            PublicKey accountKey, SecretKey macKey, String macAlgorithm, URL resource) {
126        try {
127            var keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
128
129            var innerJws = new JsonWebSignature();
130            innerJws.setPayload(keyJwk.toJson());
131            innerJws.getHeaders().setObjectHeaderValue("url", resource);
132            innerJws.getHeaders().setObjectHeaderValue("kid", kid);
133            innerJws.setAlgorithmHeaderValue(macAlgorithm);
134            innerJws.setKey(macKey);
135            innerJws.setDoKeyValidation(false);
136            innerJws.sign();
137
138            var outerClaim = new JSONBuilder();
139            outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
140            outerClaim.put("signature", innerJws.getEncodedSignature());
141            outerClaim.put("payload", innerJws.getEncodedPayload());
142            return outerClaim.toMap();
143        } catch (JoseException ex) {
144            throw new IllegalArgumentException("Could not create external account binding", ex);
145        }
146    }
147
148    /**
149     * Converts a {@link PublicKey} to a JOSE JWK structure.
150     *
151     * @param key
152     *         {@link PublicKey} to convert
153     * @return JSON map containing the JWK structure
154     */
155    public static Map<String, Object> publicKeyToJWK(PublicKey key) {
156        try {
157            return PublicJsonWebKey.Factory.newPublicJwk(key)
158                    .toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
159        } catch (JoseException ex) {
160            throw new IllegalArgumentException("Bad public key", ex);
161        }
162    }
163
164    /**
165     * Converts a JOSE JWK structure to a {@link PublicKey}.
166     *
167     * @param jwk
168     *         Map containing a JWK structure
169     * @return the extracted {@link PublicKey}
170     */
171    public static PublicKey jwkToPublicKey(Map<String, Object> jwk) {
172        try {
173            return PublicJsonWebKey.Factory.newPublicJwk(jwk).getPublicKey();
174        } catch (JoseException ex) {
175            throw new IllegalArgumentException("Bad JWK", ex);
176        }
177    }
178
179    /**
180     * Computes a thumbprint of the given public key.
181     *
182     * @param key
183     *         {@link PublicKey} to get the thumbprint of
184     * @return Thumbprint of the key
185     */
186    public static byte[] thumbprint(PublicKey key) {
187        try {
188            var jwk = PublicJsonWebKey.Factory.newPublicJwk(key);
189            return jwk.calculateThumbprint("SHA-256");
190        } catch (JoseException ex) {
191            throw new IllegalArgumentException("Bad public key", ex);
192        }
193    }
194
195    /**
196     * Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm
197     * identifier for {@link JsonWebSignature}.
198     *
199     * @param jwk
200     *         {@link JsonWebKey} to analyze
201     * @return algorithm identifier
202     * @throws IllegalArgumentException
203     *         there is no corresponding algorithm identifier for the key
204     */
205    public static String keyAlgorithm(JsonWebKey jwk) {
206        if (jwk instanceof EllipticCurveJsonWebKey) {
207            var ecjwk = (EllipticCurveJsonWebKey) jwk;
208
209            switch (ecjwk.getCurveName()) {
210                case "P-256":
211                    return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256;
212
213                case "P-384":
214                    return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384;
215
216                case "P-521":
217                    return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512;
218
219                default:
220                    throw new IllegalArgumentException("Unknown EC name "
221                            + ecjwk.getCurveName());
222            }
223
224        } else if (jwk instanceof RsaJsonWebKey) {
225            return AlgorithmIdentifiers.RSA_USING_SHA256;
226
227        } else {
228            throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm());
229        }
230    }
231
232    /**
233     * Analyzes the {@link SecretKey}, and returns the key algorithm identifier for {@link
234     * JsonWebSignature}.
235     *
236     * @param macKey
237     *         {@link SecretKey} to analyze
238     * @return algorithm identifier
239     * @throws IllegalArgumentException
240     *         there is no corresponding algorithm identifier for the key
241     */
242    public static String macKeyAlgorithm(SecretKey macKey) {
243        if (!"HMAC".equals(macKey.getAlgorithm())) {
244            throw new IllegalArgumentException("Bad algorithm: " + macKey.getAlgorithm());
245        }
246
247        var size = macKey.getEncoded().length * 8;
248        if(size < 256) {
249            throw new IllegalArgumentException("Bad key size: " + size);
250        }
251        if (size >= 512) {
252            return AlgorithmIdentifiers.HMAC_SHA512;
253        } else if (size >= 384) {
254            return AlgorithmIdentifiers.HMAC_SHA384;
255        } else {
256            return AlgorithmIdentifiers.HMAC_SHA256;
257        }
258    }
259}