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}