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}