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 *
037 * @since 2.7
038 */
039public final class JoseUtils {
040
041    private static final Logger LOG = LoggerFactory.getLogger(JoseUtils.class);
042
043    private JoseUtils() {
044        // Utility class without constructor
045    }
046
047    /**
048     * Creates an ACME JOSE request.
049     *
050     * @param url
051     *         {@link URL} of the ACME call
052     * @param keypair
053     *         {@link KeyPair} to sign the request with
054     * @param payload
055     *         ACME JSON payload. If {@code null}, a POST-as-GET request is generated
056     *         instead.
057     * @param nonce
058     *         Nonce to be used. {@code null} if no nonce is to be used in the JOSE
059     *         header.
060     * @param kid
061     *         kid to be used in the JOSE header. If {@code null}, a jwk header of the
062     *         given key is used instead.
063     * @return JSON structure of the JOSE request, ready to be sent.
064     */
065    public static JSONBuilder createJoseRequest(URL url, KeyPair keypair,
066                @Nullable JSONBuilder payload, @Nullable String nonce, @Nullable String kid) {
067        try {
068            PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
069
070            JsonWebSignature jws = new JsonWebSignature();
071            jws.getHeaders().setObjectHeaderValue("url", url);
072
073            if (kid != null) {
074                jws.getHeaders().setObjectHeaderValue("kid", kid);
075            } else {
076                jws.getHeaders().setJwkHeaderValue("jwk", jwk);
077            }
078
079            if (nonce != null) {
080                jws.getHeaders().setObjectHeaderValue("nonce", nonce);
081            }
082
083            jws.setPayload(payload != null ? payload.toString() : "");
084            jws.setAlgorithmHeaderValue(keyAlgorithm(jwk));
085            jws.setKey(keypair.getPrivate());
086            jws.sign();
087
088            if (LOG.isDebugEnabled()) {
089                LOG.debug("{} {}", payload != null ? "POST" : "POST-as-GET", url);
090                if (payload != null) {
091                    LOG.debug("  Payload: {}", payload);
092                }
093                LOG.debug("  JWS Header: {}", jws.getHeaders().getFullHeaderAsJsonString());
094            }
095
096            JSONBuilder jb = new JSONBuilder();
097            jb.put("protected", jws.getHeaders().getEncodedHeader());
098            jb.put("payload", jws.getEncodedPayload());
099            jb.put("signature", jws.getEncodedSignature());
100            return jb;
101        } catch (JoseException ex) {
102            throw new IllegalArgumentException("Could not create a JOSE request", ex);
103        }
104    }
105
106    /**
107     * Creates a JSON structure for external account binding.
108     *
109     * @param kid
110     *         Key Identifier provided by the CA
111     * @param accountKey
112     *         {@link PublicKey} of the account to register
113     * @param macKey
114     *         {@link SecretKey} to sign the key identifier with
115     * @param resource
116     *         "newAccount" resource URL
117     * @return Created JSON structure
118     */
119    public static Map<String, Object> createExternalAccountBinding(String kid,
120            PublicKey accountKey, SecretKey macKey, URL resource) {
121        try {
122            PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
123
124            JsonWebSignature innerJws = new JsonWebSignature();
125            innerJws.setPayload(keyJwk.toJson());
126            innerJws.getHeaders().setObjectHeaderValue("url", resource);
127            innerJws.getHeaders().setObjectHeaderValue("kid", kid);
128            innerJws.setAlgorithmHeaderValue(macKeyAlgorithm(macKey));
129            innerJws.setKey(macKey);
130            innerJws.setDoKeyValidation(false);
131            innerJws.sign();
132
133            JSONBuilder outerClaim = new JSONBuilder();
134            outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
135            outerClaim.put("signature", innerJws.getEncodedSignature());
136            outerClaim.put("payload", innerJws.getEncodedPayload());
137            return outerClaim.toMap();
138        } catch (JoseException ex) {
139            throw new IllegalArgumentException("Could not create external account binding", ex);
140        }
141    }
142
143    /**
144     * Converts a {@link PublicKey} to a JOSE JWK structure.
145     *
146     * @param key
147     *         {@link PublicKey} to convert
148     * @return JSON map containing the JWK structure
149     */
150    public static Map<String, Object> publicKeyToJWK(PublicKey key) {
151        try {
152            return PublicJsonWebKey.Factory.newPublicJwk(key)
153                    .toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
154        } catch (JoseException ex) {
155            throw new IllegalArgumentException("Bad public key", ex);
156        }
157    }
158
159    /**
160     * Converts a JOSE JWK structure to a {@link PublicKey}.
161     *
162     * @param jwk
163     *         Map containing a JWK structure
164     * @return the extracted {@link PublicKey}
165     */
166    public static PublicKey jwkToPublicKey(Map<String, Object> jwk) {
167        try {
168            return PublicJsonWebKey.Factory.newPublicJwk(jwk).getPublicKey();
169        } catch (JoseException ex) {
170            throw new IllegalArgumentException("Bad JWK", ex);
171        }
172    }
173
174    /**
175     * Computes a thumbprint of the given public key.
176     *
177     * @param key
178     *         {@link PublicKey} to get the thumbprint of
179     * @return Thumbprint of the key
180     */
181    public static byte[] thumbprint(PublicKey key) {
182        try {
183            PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(key);
184            return jwk.calculateThumbprint("SHA-256");
185        } catch (JoseException ex) {
186            throw new IllegalArgumentException("Bad public key", ex);
187        }
188    }
189
190    /**
191     * Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm
192     * identifier for {@link JsonWebSignature}.
193     *
194     * @param jwk
195     *         {@link JsonWebKey} to analyze
196     * @return algorithm identifier
197     * @throws IllegalArgumentException
198     *         there is no corresponding algorithm identifier for the key
199     */
200    public static String keyAlgorithm(JsonWebKey jwk) {
201        if (jwk instanceof EllipticCurveJsonWebKey) {
202            EllipticCurveJsonWebKey ecjwk = (EllipticCurveJsonWebKey) jwk;
203
204            switch (ecjwk.getCurveName()) {
205                case "P-256":
206                    return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256;
207
208                case "P-384":
209                    return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384;
210
211                case "P-521":
212                    return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512;
213
214                default:
215                    throw new IllegalArgumentException("Unknown EC name "
216                            + ecjwk.getCurveName());
217            }
218
219        } else if (jwk instanceof RsaJsonWebKey) {
220            return AlgorithmIdentifiers.RSA_USING_SHA256;
221
222        } else {
223            throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm());
224        }
225    }
226
227    /**
228     * Analyzes the {@link SecretKey}, and returns the key algorithm identifier for {@link
229     * JsonWebSignature}.
230     *
231     * @param macKey
232     *         {@link SecretKey} to analyze
233     * @return algorithm identifier
234     * @throws IllegalArgumentException
235     *         there is no corresponding algorithm identifier for the key
236     */
237    public static String macKeyAlgorithm(SecretKey macKey) {
238        if (!"HMAC".equals(macKey.getAlgorithm())) {
239            throw new IllegalArgumentException("Bad algorithm: " + macKey.getAlgorithm());
240        }
241
242        int size = macKey.getEncoded().length * 8;
243        switch (size) {
244            case 256:
245                return AlgorithmIdentifiers.HMAC_SHA256;
246
247            case 384:
248                return AlgorithmIdentifiers.HMAC_SHA384;
249
250            case 512:
251                return AlgorithmIdentifiers.HMAC_SHA512;
252
253            default:
254                throw new IllegalArgumentException("Bad key size: " + size);
255        }
256    }
257
258}