All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nimbusds.openid.connect.provider.jwkset.gen.JWKSetGenerator Maven / Gradle / Ivy

package com.nimbusds.openid.connect.provider.jwkset.gen;


import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;

import com.thetransactioncompany.json.pretty.PrettyJson;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jose.util.JSONObjectUtils;
import com.nimbusds.openid.connect.provider.jwkset.JWKSetSpec;


/**
 * JWK set generator for Connect2id servers.
 */
public class JWKSetGenerator {
	
	
	/**
	 * Generates a new set of rotating signature and encryption keys for a
	 * Connect2id server.
	 *
	 * @param reservedKeyIDs   The reserved key IDs, empty if none.
	 * @param eventMessageSink Optional sink for event messages,
	 *                         {@code null} if not specified.
	 *
	 * @return The generated rotating keys.
	 */
	public List generateRotatingKeys(final KeyIDs reservedKeyIDs, final Consumer eventMessageSink)
		throws JOSEException {
		
		List keys = new LinkedList<>();
		
		KeyIDs keyIDs = new KeyIDs();
		keyIDs.addAll(reservedKeyIDs);
		
		RSAKey rsaKey = JWKSetSpec.RotatedRSASigning.generateKey(keyIDs.addRandomUniqueKeyID());
		keys.add(rsaKey);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new signing RSA " + JWKSetSpec.RotatedRSASigning.KEY_BIT_SIZE + " bit key with ID " + rsaKey.getKeyID());
		}
		
		for (Curve crv: JWKSetSpec.RotatedECSigning.SUPPORTED_CURVES) {
			ECKey ecKey = JWKSetSpec.RotatedECSigning.generateKey(crv, keyIDs.addRandomUniqueKeyID());
			keys.add(ecKey);
			if (eventMessageSink != null) {
				eventMessageSink.accept("Generated new signing EC " + ecKey.getCurve() + " key with ID " + ecKey.getKeyID());
			}
		}
		
		OctetKeyPair okp = JWKSetSpec.RotatedEdDSASigning.generateKey(keyIDs.addRandomUniqueKeyID());
		keys.add(okp);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new signing " + okp.getCurve() + " key with ID " + okp.getKeyID());
		}
		
		rsaKey = JWKSetSpec.RotatedRSAEncryption.generateKey(keyIDs.addRandomUniqueKeyID());
		keys.add(rsaKey);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new encryption RSA " + JWKSetSpec.RotatedRSASigning.KEY_BIT_SIZE + " bit key with ID " + rsaKey.getKeyID());
		}
		
		for (Curve crv: JWKSetSpec.RotatedECDHEncryption.SUPPORTED_CURVES) {
			ECKey ecKey = JWKSetSpec.RotatedECDHEncryption.generateKey(crv, keyIDs.addRandomUniqueKeyID());
			keys.add(ecKey);
			if (eventMessageSink != null) {
				eventMessageSink.accept("Generated new encryption EC " + ecKey.getCurve() + " key with ID " + ecKey.getKeyID());
			}
		}
		
		OctetSequenceKey secretKey = JWKSetSpec.RotatedAccessTokenDirectEncryption.generateKey(keyIDs.addRandomUniqueKeyID());
		keys.add(secretKey);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new encryption AES " + JWKSetSpec.RotatedAccessTokenDirectEncryption.KEY_BIT_SIZES[0] + " bit key with ID " + secretKey.getKeyID());
		}
		
		return keys;
	}
	
	
	/**
	 * Generates a new set of permanent keys for a Connect2id server.
	 *
	 * @param eventMessageSink Optional sink for event messages,
	 *                         {@code null} if not specified.
	 *
	 * @return The generated keys.
	 */
	public List generatePermanentKeys(final Consumer eventMessageSink)
		throws JOSEException {
		
		List keys = new LinkedList<>();
		
		OctetSequenceKey hmacKey = JWKSetSpec.HMAC.generateKey();
		keys.add(hmacKey);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new HMAC SHA " + JWKSetSpec.HMAC.KEY_BIT_SIZE + " bit key with ID " + hmacKey.getKeyID());
		}
		
		OctetSequenceKey subjectKey = JWKSetSpec.SubjectEncryption.generateKey();
		keys.add(subjectKey);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new subject encryption AES SIV " + JWKSetSpec.SubjectEncryption.KEY_BIT_SIZES[0] + " bit key with ID " + subjectKey.getKeyID());
		}
		
		OctetSequenceKey refreshTokenKey = JWKSetSpec.RefreshTokenEncryption.generateKey();
		keys.add(refreshTokenKey);
		if (eventMessageSink != null) {
			eventMessageSink.accept("Generated new refresh token encryption AES SIV " + JWKSetSpec.RefreshTokenEncryption.KEY_BIT_SIZE + " bit key with ID " + refreshTokenKey.getKeyID());
		}
		
		return keys;
	}
	
	
	/**
	 * Generates the missing permanent keys for a Connect2id server not
	 * found in the specified JWK set.
	 *
	 * @param jwkSet           The JWK set.
	 * @param eventMessageSink Optional sink for event messages,
	 *                         {@code null} if not specified.
	 *
	 * @return The generated missing permanent keys, empty list if none.
	 */
	public List generateMissingPermanentKeys(final JWKSet jwkSet, final Consumer eventMessageSink)
		throws JOSEException {
		
		List keys = new LinkedList<>();
		
		if (new JWKSelector(JWKSetSpec.HMAC.KEY_MATCHER).select(jwkSet).isEmpty()) {
			OctetSequenceKey hmacKey = JWKSetSpec.HMAC.generateKey();
			keys.add(hmacKey);
			if (eventMessageSink != null) {
				eventMessageSink.accept("Generated missing HMAC SHA " + JWKSetSpec.HMAC.KEY_BIT_SIZE + " bit key with ID " + hmacKey.getKeyID());
			}
		}
		
		if (new JWKSelector(JWKSetSpec.SubjectEncryption.KEY_MATCHER).select(jwkSet).isEmpty()) {
			OctetSequenceKey subjectKey = JWKSetSpec.SubjectEncryption.generateKey();
			keys.add(subjectKey);
			if (eventMessageSink != null) {
				eventMessageSink.accept("Generated missing subject encryption AES SIV " + JWKSetSpec.SubjectEncryption.KEY_BIT_SIZES[0] + " bit key with ID " + subjectKey.getKeyID());
			}
		}
		
		if (new JWKSelector(JWKSetSpec.RefreshTokenEncryption.KEY_MATCHER).select(jwkSet).isEmpty()) {
			OctetSequenceKey refreshTokenKey = JWKSetSpec.RefreshTokenEncryption.generateKey();
			keys.add(refreshTokenKey);
			if (eventMessageSink != null) {
				eventMessageSink.accept("Generated missing refresh token encryption AES SIV " + JWKSetSpec.RefreshTokenEncryption.KEY_BIT_SIZE + " bit key with ID " + refreshTokenKey.getKeyID());
			}
		}
		
		return keys;
	}
	
	
	/**
	 * Generates a new JWK set for a Connect2id server.
	 *
	 * @param eventMessageSink Optional sink for event messages,
	 *                         {@code null} if not specified.
	 *
	 * @return The JWK set.
	 */
	public JWKSet generate(final Consumer eventMessageSink)
		throws JOSEException {
		
		List keys = new LinkedList<>();
		keys.addAll(generateRotatingKeys(new KeyIDs(), eventMessageSink));
		keys.addAll(generatePermanentKeys(eventMessageSink));
		return new JWKSet(keys);
	}
	
	
	/**
	 * A generates a new set of signing and encryption keys and prefixes
	 * them to the specified Connect2id server JWK set.
	 *
	 * @param oldJWKSet        The Connect2id server JWK set. Must not be
	 *                         {@code null}.
	 * @param eventMessageSink Optional sink for event messages,
	 *                         {@code null} if not specified.
	 *
	 * @return The updated JWK set.
	 */
	public JWKSet generateAndPrefixNewKeys(final JWKSet oldJWKSet, final Consumer eventMessageSink)
		throws Exception {
		
		// Prefix so Connect2id server can roll over to new keys
		List keys = generateRotatingKeys(new KeyIDs(oldJWKSet), eventMessageSink);
		JWKSet jwkSet = prefixKeys(oldJWKSet, keys);
		
		if (eventMessageSink != null) {
			eventMessageSink.accept("Prefixed newly generated keys to existing JWK set");
		}
		
		return jwkSet;
	}
	
	
	/**
	 * Inserts the specified keys at the beginning of a JWK set.
	 *
	 * @param jwkSet The JWK set.
	 * @param keys   The keys to insert.
	 *
	 * @return The updated JWK set.
	 */
	private static JWKSet prefixKeys(final JWKSet jwkSet, final List keys) {
		
		List updatedKeyList = new LinkedList<>();
		updatedKeyList.addAll(keys);
		updatedKeyList.addAll(jwkSet.getKeys());
		return new JWKSet(updatedKeyList);
	}
	
	
	/**
	 * Appends the specified keys at the end of a JWK set.
	 *
	 * @param jwkSet The JWK set.
	 * @param keys   The keys to append.
	 *
	 * @return The updated JWK set.
	 */
	private static JWKSet postfixKeys(final JWKSet jwkSet, final List keys) {
		
		List updatedKeyList = new LinkedList<>();
		updatedKeyList.addAll(jwkSet.getKeys());
		updatedKeyList.addAll(keys);
		return new JWKSet(updatedKeyList);
	}
	
	
	/**
	 * Prints the CLI usage to standard output.
	 */
	static void printUsage() {
		System.out.println("Usage:");
		System.out.println("1) Generate a new Connect2id server JWK set: ");
		System.out.println("   java -jar jwkset-gen.jar jwkSet.json");
		System.out.println("2) Generate a new set of rotating keys and add to an existing Connect2id server\n" +
				   "   JWK set; if a required permanent key is found missing it will also be\n" +
			           "   generated and appended to the JWK set (useful when upgrading to a newer\n" +
				   "   Connect2id server release requiring a new type of permanent key):");
		System.out.println("   java -jar jwkset-gen.jar oldJWKSet.json newJWKSet.json");
		System.out.println("3) BASE64URL encode the output JWK set: ");
		System.out.println("   java -jar -b64 jwkset-gen.jar oldJWKSet.json newJWKSet.json.b64");
		System.out.println("4) Generate a new OpenID Connect Federation 1.0 entity JWK set: ");
		System.out.println("   java -jar jwkset-gen.jar -federation federationJWKSet.json");
		System.out.println("5) Generate a new set of rotating keys and add to an existing OpenID Connect\n" +
				   "   Federation 1.0 entity JWK set: ");
		System.out.println("   java -jar jwkset-gen.jar -federation oldFedJWKSet.json newFedJWKSet.json");
	}
	
	
	/**
	 * Console method for generating a new Connect2id server JWK set, or
	 * updating an existing JWK set with new signing and encryption keys.
	 *
	 * @param args The command line arguments.
	 */
	public static void main(final String[] args) {
		
		String softwareVersion = JWKSetGenerator.class.getPackage().getImplementationVersion();
		
		System.out.println("JWK set generator " + (softwareVersion != null ? "v" + softwareVersion + " " : "") + "for Connect2id server v6.x+");
		
		// create Options object
		Options options = new Options();
		options.addOption("b64", false, "BASE64URL encode the output JWK set");
		options.addOption("federation", false, "Create federation entity JWK set");
		
		CommandLineParser parser = new DefaultParser();
		CommandLine commandLine;
		try {
			commandLine = parser.parse( options, args);
		} catch (org.apache.commons.cli.ParseException e) {
			System.err.println( "Command line parse error: " + e.getMessage());
			printUsage();
			return;
		}
		
		final boolean b64Encode = commandLine.hasOption("b64");
		final boolean federation = commandLine.hasOption("federation");
		
		final List argList = commandLine.getArgList();
		
		final File oldJWKSetFile;
		final File newJWKSetFile;
		
		if (argList.size() == 2) {
			oldJWKSetFile = new File(argList.get(0));
			newJWKSetFile = new File(argList.get(1));
		} else if (argList.size() == 1) {
			oldJWKSetFile = null;
			newJWKSetFile = new File(argList.get(0));
		} else {
			printUsage();
			return;
		}
		
		JWKSet oldJWKSet = null;
		
		if (oldJWKSetFile != null) {
			try {
				oldJWKSet = JWKSet.load(oldJWKSetFile);
			} catch (IOException | ParseException e) {
				System.err.println("Couldn't read old JWK set file: " + e.getMessage());
				return;
			}
		}
		
		final Consumer eventMessageSink = new Consumer() {
			private final NumberedEventPrinter printer = new NumberedEventPrinter();
			@Override
			public void accept(final String eventMessage) {
				printer.print(eventMessage);
			}
		};
		
		JWKSet newJWKSet;
		try {
			if (oldJWKSet == null) {
				// Generate entire new JWK set
				if (federation) {
					// Federation entity keys
					newJWKSet = new FederationJWKSetGenerator().generate(eventMessageSink);
				} else {
					// Connect2id server keys
					newJWKSet = new JWKSetGenerator().generate(eventMessageSink);
				}
			} else {
				// Prefix new rotating keys to existing JWK set
				if (federation) {
					// Federation entity keys
					newJWKSet = new FederationJWKSetGenerator().generateAndPrefixNewKeys(oldJWKSet, eventMessageSink);
				} else {
					// Connect2id server keys
					JWKSetGenerator gen = new JWKSetGenerator();
					newJWKSet = gen.generateAndPrefixNewKeys(oldJWKSet, eventMessageSink);
					
					// Check if any of the required permanent keys are missing after an upgrade to a newer
					// Connect2id server and generate them
					List missingPermanentKeys = gen.generateMissingPermanentKeys(newJWKSet, eventMessageSink);
					if (! missingPermanentKeys.isEmpty()) {
						newJWKSet = postfixKeys(newJWKSet, missingPermanentKeys);
						eventMessageSink.accept("Appended generated permanent keys to existing JWK set");
					}
				}
			}
		} catch (Exception e) {
			System.err.println("Couldn't generate JWK key: " + e.getMessage());
			return;
		}
		
		String json = JSONObjectUtils.toJSONString(newJWKSet.toJSONObject(false));
		
		String output;
		
		if (b64Encode) {
			output = Base64URL.encode(json).toString();
		} else {
			try {
				output = new PrettyJson(PrettyJson.Style.COMPACT).parseAndFormat(json);
			} catch (ParseException e) {
				System.err.println("Couldn't format JSON: " + e.getMessage());
				return;
			}
		}
		
		try {
			PrintWriter writer = new PrintWriter(newJWKSetFile, "UTF-8");
			writer.write(output);
			writer.write("\n");
			writer.close();
		} catch (IOException e) {
			System.err.println("Couldn't write new JWK set file: " + e.getMessage());
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy