WebCryptoHeader.java

/*
 * Copyright © 2025 Indiana University
 * All rights reserved.
 *
 * BSD 3-Clause License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * - Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.iu.crypt;

import java.net.URI;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;

import edu.iu.IuIterable;
import edu.iu.IuObject;
import edu.iu.crypt.WebKey.Algorithm;
import edu.iu.crypt.WebKey.Use;

/**
 * Unifies algorithm support and maps cryptographic header data from JCE to JSON
 * Object Signing and Encryption (JOSE).
 * 
 * @see <a href="https://datatracker.ietf.org/doc/html/rfc7517">JSON Web Key
 *      (JWK) RFC-7517</a>
 */
public interface WebCryptoHeader extends WebCertificateReference {

	/**
	 * Enumerates standard header parameters.
	 */
	enum Param {
		/**
		 * Encryption/signature algorithm.
		 */
		ALGORITHM("alg", EnumSet.allOf(Use.class), true, WebCryptoHeader::getAlgorithm),

		/**
		 * Well-known key identifier.
		 */
		KEY_ID("kid", EnumSet.allOf(Use.class), false, WebCryptoHeader::getKeyId),

		/**
		 * Well-known key set URI.
		 */
		KEY_SET_URI("jku", EnumSet.allOf(Use.class), false, WebCryptoHeader::getKeySetUri),

		/**
		 * Well-known public key.
		 */
		KEY("jwk", EnumSet.allOf(Use.class), false, WebCryptoHeader::getKey),

		/**
		 * Certificate chain URI.
		 */
		CERTIFICATE_URI("x5u", EnumSet.allOf(Use.class), false, WebCryptoHeader::getCertificateUri),

		/**
		 * Certificate chain.
		 */
		CERTIFICATE_CHAIN("x5c", EnumSet.allOf(Use.class), false, WebCryptoHeader::getCertificateChain),

		/**
		 * Certificate SHA-1 thumb print.
		 */
		CERTIFICATE_THUMBPRINT("x5t", EnumSet.allOf(Use.class), false, WebCryptoHeader::getCertificateThumbprint),

		/**
		 * Certificate SHA-1 thumb print.
		 */
		CERTIFICATE_SHA256_THUMBPRINT("x5t#S256", EnumSet.allOf(Use.class), false,
				WebCryptoHeader::getCertificateSha256Thumbprint),

		/**
		 * Signature/encryption media type.
		 */
		TYPE("typ", EnumSet.allOf(Use.class), false, WebCryptoHeader::getType),

		/**
		 * Content media type.
		 */
		CONTENT_TYPE("cty", EnumSet.allOf(Use.class), false, WebCryptoHeader::getContentType),

		/**
		 * Extended parameter names that <em>must</em> be included in the protected
		 * header.
		 */
		CRITICAL_PARAMS("crit", EnumSet.allOf(Use.class), false, WebCryptoHeader::getCriticalParameters),

		/**
		 * Content encryption algorithm.
		 */
		ENCRYPTION("enc", EnumSet.of(Use.ENCRYPT), true, a -> a.getExtendedParameter("enc")),

		/**
		 * Plain-text compression algorithm for encryption.
		 */
		ZIP("zip", EnumSet.of(Use.ENCRYPT), false, a -> a.getExtendedParameter("zip")),

		/**
		 * Ephemeral public key for key agreement algorithms.
		 * 
		 * @see Algorithm#ECDH_ES
		 * @see Algorithm#ECDH_ES_A128KW
		 * @see Algorithm#ECDH_ES_A192KW
		 * @see Algorithm#ECDH_ES_A256KW
		 */
		EPHEMERAL_PUBLIC_KEY("epk", EnumSet.of(Use.ENCRYPT), true, a -> a.getExtendedParameter("epk")),

		/**
		 * Public originator identifier (PartyUInfo) for key derivation.
		 * 
		 * @see Algorithm#ECDH_ES
		 * @see Algorithm#ECDH_ES_A128KW
		 * @see Algorithm#ECDH_ES_A192KW
		 * @see Algorithm#ECDH_ES_A256KW
		 */
		PARTY_UINFO("apu", EnumSet.of(Use.ENCRYPT), false, a -> a.getExtendedParameter("apu")),

		/**
		 * Public recipient identifier (PartyVInfo) for key derivation.
		 * 
		 * @see Algorithm#ECDH_ES
		 * @see Algorithm#ECDH_ES_A128KW
		 * @see Algorithm#ECDH_ES_A192KW
		 * @see Algorithm#ECDH_ES_A256KW
		 */
		PARTY_VINFO("apv", EnumSet.of(Use.ENCRYPT), false, a -> a.getExtendedParameter("apv")),

		/**
		 * Initialization vector for GCM key wrap.
		 * 
		 * @see Algorithm#A128GCMKW
		 * @see Algorithm#A192GCMKW
		 * @see Algorithm#A256GCMKW
		 */
		INITIALIZATION_VECTOR("iv", EnumSet.of(Use.ENCRYPT), true, a -> a.getExtendedParameter("iv")),

		/**
		 * Authentication tag for GCM key wrap.
		 * 
		 * @see Algorithm#A128GCMKW
		 * @see Algorithm#A192GCMKW
		 * @see Algorithm#A256GCMKW
		 */
		TAG("tag", Set.of(Use.ENCRYPT), true, a -> a.getExtendedParameter("tag")),

		/**
		 * Password salt for use with PBES2.
		 * 
		 * @see Algorithm#PBES2_HS256_A128KW
		 * @see Algorithm#PBES2_HS384_A192KW
		 * @see Algorithm#PBES2_HS512_A256KW
		 */
		PASSWORD_SALT("p2s", Set.of(Use.ENCRYPT), true, a -> a.getExtendedParameter("p2s")),

		/**
		 * PBKDF2 iteration count for use with PBES2.
		 * 
		 * @see Algorithm#PBES2_HS256_A128KW
		 * @see Algorithm#PBES2_HS384_A192KW
		 * @see Algorithm#PBES2_HS512_A256KW
		 */
		PASSWORD_COUNT("p2c", Set.of(Use.ENCRYPT), true, a -> a.getExtendedParameter("p2c"));

		/**
		 * Gets a parameter by JOSE standard parameter name.
		 * 
		 * @param name JOSE standard name
		 * @return {@link Param}
		 */
		public static Param from(String name) {
			return EnumSet.allOf(Param.class).stream().filter(a -> IuObject.equals(name, a.name)).findFirst()
					.orElse(null);
		}

		/**
		 * JOSE standard parameter name.
		 */
		public final String name;

		/**
		 * Indicates if the parameter name is registered for use with <a href=
		 * "https://datatracker.ietf.org/doc/html/rfc7515#section-4.1">signature</a>
		 * and/or <a href=
		 * "https://datatracker.ietf.org/doc/html/rfc7515#section-4.1">encryption</a>.
		 */
		private final Set<Use> use;

		/**
		 * Indicates if the parameter is required for this algorithm.
		 */
		public final boolean required;

		private final Function<WebCryptoHeader, ?> get;

		private Param(String name, Set<Use> use, boolean required, Function<WebCryptoHeader, ?> get) {
			this.name = name;
			this.use = Collections.unmodifiableSet(use);
			this.required = required;
			this.get = get;
		}

		/**
		 * Verifies that the header contains a non-null value.
		 * 
		 * @param header header
		 * @return true if the value is present; else false
		 */
		public boolean isPresent(WebCryptoHeader header) {
			return get(header) != null;
		}

		/**
		 * Determines if a header applies to a public JWK use case.
		 * 
		 * @param use public key use
		 * @return true if the header parameter is registered for the public JWK use
		 *         case.
		 */
		public boolean isUsedFor(Use use) {
			return this.use.contains(use);
		}

		/**
		 * Gets the header value.
		 * 
		 * @param header header
		 * @return header value
		 */
		public Object get(WebCryptoHeader header) {
			return get.apply(header);
		}
	}

	/**
	 * Builder interface for creating {@link WebCryptoHeader} instances.
	 * 
	 * @param <B> builder type
	 */
	interface Builder<B extends Builder<B>> {
		/**
		 * Sets the key ID relative to {@link #getKeySetUri()} corresponding to a JWKS
		 * key entry.
		 *
		 * @param keyId key ID
		 * @return this
		 */
		B keyId(String keyId);

		/**
		 * Sets the URI where JWKS well-known key data can be retrieved.
		 * 
		 * @param uri JWKS {@link URI}
		 * @return this
		 */
		B wellKnown(URI uri);

		/**
		 * Sets the key to include with the header.
		 * 
		 * @param key <em>may</em> include private/secret key data to use for
		 *            encryption/signing; only {@link WebKey#wellKnown()} will be
		 *            included in the header.
		 * @return this
		 */
		B wellKnown(WebKey key);

		/**
		 * The key to use for encrypting or signing.
		 * 
		 * @param key key to use for encryption or signing; will not be included in the
		 *            header. Use {@link #wellKnown(WebKey)} to set the key and include
		 *            well-known component in the header.
		 * @return this
		 */
		B key(WebKey key);

		/**
		 * Sets the header type parameter value.
		 * 
		 * @param type header type parameter value.
		 * @return this
		 */
		B type(String type);

		/**
		 * Sets the header content type parameter value.
		 * 
		 * @param contentType header type parameter value.
		 * @return this
		 */
		B contentType(String contentType);

		/**
		 * Sets critical parameter names.
		 * 
		 * @param parameterNames critical parameter names
		 * @return this
		 */
		B crit(String... parameterNames);

		/**
		 * Sets a registered parameter value
		 * 
		 * @param <T>   value type
		 * @param param parameter
		 * @param value parameter value
		 * @return this
		 */
		<T> B param(Param param, T value);

		/**
		 * Sets an extended parameter value
		 * 
		 * @param <T>   value type
		 * @param name  parameter name
		 * @param value parameter value
		 * @return this
		 */
		<T> B param(String name, T value);
	}

	/**
	 * Returns the protected header from a serialized JWS or JWE.
	 * 
	 * <p>
	 * This method is useful for inspecting header parameters before processing the
	 * signed and/or encrypted payload, e.g., to determine content type or to
	 * identify the correct decryption or verification key.
	 * </p>
	 * 
	 * @param serialized serialized JWS or JWE
	 * @return protected header
	 */
	public static WebCryptoHeader getProtectedHeader(String serialized) {
		return Init.SPI.getProtectedHeader(serialized);
	}

	/**
	 * Verifies all parameters in a {@link WebCryptoHeader}.
	 * 
	 * @param header {@link WebCryptoHeader}
	 * @return Well-known key referred to by the header; null if not known
	 */
	static WebKey verify(WebCryptoHeader header) {
		final var algorithm = Objects.requireNonNull(header.getAlgorithm(),
				() -> "Signature or key protection algorithm is required");

		if (algorithm.use.equals(Use.ENCRYPT)) {
			Objects.requireNonNull(header.getExtendedParameter(Param.ENCRYPTION.name),
					() -> "Content encryption algorithm is required");
			for (final var param : algorithm.encryptionParams)
				if (param.required //
						&& !param.isPresent(header))
					throw new NullPointerException("Missing required encryption parameter " + param.name);
		}

		final var criticalParameters = header.getCriticalParameters();
		if (criticalParameters != null)
			for (final var paramName : criticalParameters) {
				final var param = Param.from(paramName);
				if (param == null) {
					if (header.getExtendedParameter(paramName) == null)
						throw new NullPointerException("Missing critical extended parameter " + paramName);
				} else if (!param.isPresent(header))
					throw new NullPointerException("Missing critical registered parameter " + paramName);
			}

		final var key = header.getKey();
		final var keyId = IuObject.first(header.getKeyId(), IuObject.convert(key, WebKey::getKeyId));
		final var certChain = WebCertificateReference.verify(header);

		var wellKnown = IuObject.convert(key, WebKey::wellKnown);
		if (wellKnown == null //
				&& keyId != null)
			wellKnown = IuObject.convert(header.getKeySetUri(), //
					uri -> IuIterable.filter(WebKey.readJwks(uri), //
							k -> keyId.equals(k.getKeyId())).iterator().next());
		if (wellKnown == null //
				&& certChain != null)
			wellKnown = WebKey.builder(algorithm.type[0]).cert(certChain).build().wellKnown();

		IuObject.first( //
				IuObject.convert(wellKnown, WebKey::getPublicKey), //
				IuObject.convert(certChain, c -> c[0].getPublicKey()));

		return wellKnown;
	}

	/**
	 * Gets the cryptographic algorithm.
	 * 
	 * @return {@link Algorithm}
	 */
	Algorithm getAlgorithm();

	/**
	 * Gets the key ID relative to {@link #getKeySetUri()} corresponding to a JWKS
	 * key entry.
	 * 
	 * @return key ID
	 */
	String getKeyId();

	/**
	 * Gets the URI where JWKS well-known key data can be retrieved.
	 * 
	 * @return {@link URI}
	 */
	URI getKeySetUri();

	/**
	 * Gets the well-known key data.
	 * 
	 * @return {@link WebKey}
	 */
	WebKey getKey();

	/**
	 * Gets the header type parameter value.
	 * 
	 * @return header type parameter value.
	 */
	String getType();

	/**
	 * Gets the header type parameter value.
	 * 
	 * @return header type parameter value.
	 */
	String getContentType();

	/**
	 * Gets the set of critical parameter names.
	 * 
	 * @return critical parameter names
	 */
	Set<String> getCriticalParameters();

	/**
	 * Gets extended parameters.
	 * 
	 * @param <T>  parameter type
	 * @param name parameter name
	 * @return extended parameters
	 */
	<T> T getExtendedParameter(String name);

}