package at.gv.egiz.eaaf.modules.auth.sl20.utils;
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jose4j.jca.ProviderContext;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwx.Headers;
import org.jose4j.jwx.JsonWebStructure;
import org.jose4j.keys.resolvers.X509VerificationKeyResolver;
import org.jose4j.lang.JoseException;
import org.springframework.util.Base64Utils;
import at.gv.egiz.eaaf.core.exception.EaafKeyUsageException;
import at.gv.egiz.eaaf.core.exceptions.EaafException;
import at.gv.egiz.eaaf.core.impl.credential.EaafKeyStoreUtils;
import at.gv.egiz.eaaf.core.impl.data.Pair;
import at.gv.egiz.eaaf.core.impl.utils.X509Utils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* {@link JoseUtils} provides static methods JWS and JWE processing.
*
* @author tlenz
*
*/
@Slf4j
public class JoseUtils {
private static final Provider provider = new BouncyCastleProvider();
/**
* Create a JWS signature.
*
*
* Use {@link org.jose4j.jws.AlgorithmIdentifiers.RSA_PSS_USING_SHA256} in case
* of a RSA based key and
* {@link org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256}
* in case of an ECC based key.
*
*
* @param keyStore KeyStore that should be used
* @param keyAlias Alias of the private key
* @param keyPassword Password to access the key
* @param payLoad PayLoad to sign
* @param addFullCertChain If true the full certificate chain will be
* added, otherwise only the
* X509CertSha256Fingerprint is added into JOSE
* header
* @param friendlyNameForLogging FriendlyName for the used KeyStore for logging
* purposes only
* @return Signed PayLoad in serialized form
* @throws EaafException In case of a key-access or key-usage error
* @throws JoseException In case of a JOSE error
*/
public static String createSignature(@Nonnull Pair keyStore,
@Nonnull final String keyAlias, @Nonnull final char[] keyPassword,
@Nonnull final String payLoad, boolean addFullCertChain,
@Nonnull String friendlyNameForLogging) throws EaafException, JoseException {
return createSignature(keyStore, keyAlias, keyPassword, payLoad, addFullCertChain, Collections.emptyMap(),
AlgorithmIdentifiers.RSA_PSS_USING_SHA256, AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256,
friendlyNameForLogging);
}
/**
* Create a JWS signature.
*
*
* Use {@link org.jose4j.jws.AlgorithmIdentifiers.RSA_PSS_USING_SHA256} in case
* of a RSA based key and
* {@link org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256}
* in case of an ECC based key.
*
*
* @param keyStore KeyStore that should be used
* @param keyAlias Alias of the private key
* @param keyPassword Password to access the key
* @param payLoad PayLoad to sign
* @param addFullCertChain If true the full certificate chain will be
* added, otherwise only the
* X509CertSha256Fingerprint is added into JOSE
* header
* @param joseHeaders HeaderName and HeaderValue that should be set
* into JOSE header
* @param friendlyNameForLogging FriendlyName for the used KeyStore for logging
* purposes only
* @return Signed PayLoad in serialized form
* @throws EaafException In case of a key-access or key-usage error
* @throws JoseException In case of a JOSE error
*/
public static String createSignature(@Nonnull Pair keyStore,
@Nonnull final String keyAlias, @Nonnull final char[] keyPassword,
@Nonnull final String payLoad, boolean addFullCertChain,
@Nonnull final Map joseHeaders,
@Nonnull String friendlyNameForLogging) throws EaafException, JoseException {
return createSignature(keyStore, keyAlias, keyPassword, payLoad, addFullCertChain, joseHeaders,
AlgorithmIdentifiers.RSA_PSS_USING_SHA256, AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256,
friendlyNameForLogging);
}
/**
* Create a JWS signature.
*
* @param keyStore KeyStore that should be used
* @param keyAlias Alias of the private key
* @param keyPassword Password to access the key
* @param payLoad PayLoad to sign
* @param addFullCertChain If true the full certificate chain will be
* added, otherwise only the
* X509CertSha256Fingerprint is added into JOSE
* header
* @param joseHeaders HeaderName and HeaderValue that should be set
* into JOSE header
* @param rsaAlgToUse Signing algorithm that should be used in case
* of a signing key based on RSA
* @param eccAlgToUse Signing algorithm that should be used in case
* of a signing key based on ECC
* @param friendlyNameForLogging FriendlyName for the used KeyStore for logging
* purposes only
* @return Signed PayLoad in serialized form
* @throws EaafException In case of a key-access or key-usage error
* @throws JoseException In case of a JOSE error
*/
public static String createSignature(@Nonnull Pair keyStore,
@Nonnull final String keyAlias, @Nonnull final char[] keyPassword,
@Nonnull final String payLoad, boolean addFullCertChain,
@Nonnull final Map joseHeaders,
@Nonnull final String rsaAlgToUse, @Nonnull final String eccAlgToUse,
@Nonnull String friendlyNameForLogging) throws EaafException, JoseException {
final JsonWebSignature jws = new JsonWebSignature();
// set payload
jws.setPayload(payLoad);
// set JOSE headers
for (final Entry el : joseHeaders.entrySet()) {
log.trace("Set JOSE header: {} with value: {} into JWS", el.getKey(), el.getValue());
jws.setHeader(el.getKey(), el.getValue());
}
// set signing information
final Pair signingCred = EaafKeyStoreUtils.getPrivateKeyAndCertificates(
keyStore.getFirst(), keyAlias, keyPassword, true, friendlyNameForLogging);
// set verification key
jws.setKey(convertToBcKeyIfRequired(signingCred.getFirst()));
jws.setAlgorithmHeaderValue(getKeyOperationAlgorithmFromCredential(
jws.getKey(), rsaAlgToUse, eccAlgToUse, friendlyNameForLogging));
// set special provider if required
if (keyStore.getSecond() != null) {
log.trace("Injecting special Java Security Provider: {}", keyStore.getSecond().getName());
final ProviderContext providerCtx = new ProviderContext();
providerCtx.getSuppliedKeyProviderContext().setSignatureProvider(keyStore.getSecond().getName());
providerCtx.getGeneralProviderContext().setGeneralProvider(BouncyCastleProvider.PROVIDER_NAME);
jws.setProviderContext(providerCtx);
} else {
final ProviderContext providerCtx = new ProviderContext();
providerCtx.getGeneralProviderContext().setGeneralProvider(BouncyCastleProvider.PROVIDER_NAME);
jws.setProviderContext(providerCtx);
}
if (addFullCertChain) {
jws.setCertificateChainHeaderValue(signingCred.getSecond());
}
jws.setX509CertSha256ThumbprintHeaderValue(signingCred.getSecond()[0]);
return jws.getCompactSerialization();
}
/**
* Verify a JOSE signature.
*
* @param serializedContent Serialized content that should be verified
* @param trustedCerts Trusted certificates that should be used for
* verification
* @param constraints {@link AlgorithmConstraints} for verification
* @return {@link JwsResult} object
* @throws JoseException In case of a signature verification error
* @throws IOException In case of a general error
*/
public static JwsResult validateSignature(@Nonnull final String serializedContent,
@Nonnull final List trustedCerts, @Nonnull final AlgorithmConstraints constraints)
throws JoseException, IOException {
final JsonWebSignature jws = new JsonWebSignature();
// set payload
jws.setCompactSerialization(serializedContent);
// set security constrains
jws.setAlgorithmConstraints(constraints);
// load signinc certs
Key selectedKey = null;
final List x5cCerts = jws.getCertificateChainHeaderValue();
final String x5t256 = jws.getX509CertSha256ThumbprintHeaderValue();
if (x5cCerts != null) {
log.debug("Found x509 certificate in JOSE header ... ");
log.trace("Sorting received X509 certificates ... ");
final List sortedX5cCerts = X509Utils.sortCertificates(x5cCerts);
if (trustedCerts.contains(sortedX5cCerts.get(0))) {
selectedKey = sortedX5cCerts.get(0).getPublicKey();
} else {
log.info("Can NOT find JOSE certificate in truststore.");
if (log.isDebugEnabled()) {
try {
log.debug("Cert: {}", Base64Utils.encodeToString(sortedX5cCerts.get(0).getEncoded()));
} catch (final CertificateEncodingException e) {
log.warn("Can not create DEBUG output", e);
}
}
}
} else if (StringUtils.isNotEmpty(x5t256)) {
log.debug("Found x5t256 fingerprint in JOSE header .... ");
final X509VerificationKeyResolver x509VerificationKeyResolver = new X509VerificationKeyResolver(
trustedCerts);
selectedKey = x509VerificationKeyResolver.resolveKey(jws, Collections.emptyList());
} else {
throw new JoseException("JWS contains NO signature certificate or NO certificate fingerprint");
}
if (selectedKey == null) {
throw new JoseException("Can NOT select verification key for JWS. Signature verification FAILED");
}
//set BouncyCastleProvider as default provider
final ProviderContext providerCtx = new ProviderContext();
providerCtx.getGeneralProviderContext().setGeneralProvider(BouncyCastleProvider.PROVIDER_NAME);
jws.setProviderContext(providerCtx);
// set verification key
jws.setKey(convertToBcKeyIfRequired(selectedKey));
// load payLoad
return new JwsResult(
jws.verifySignature(),
jws.getUnverifiedPayload(),
jws.getHeaders(),
x5cCerts);
}
/**
* Convert an ECC public-key into BouncyCastle implementation.
*
* IAIK JCE / Eccelerate ECC Keys are not compatible to JWS impl.
* @param input Key
* @return input Key, or BC ECC-Key in case of a ECC Key
*/
public static Key convertToBcKeyIfRequired(Key input) {
try {
if (input instanceof ECPublicKey
&& "iaik.security.ec.common.ECPublicKey".equals(input.getClass().getName())) {
//convert Key to BouncyCastle KeyImplemenation because there is an
//incompatibility with IAIK EC Keys and JWS signature-verfification implementation
PublicKey publicKey = KeyFactory.getInstance(
input.getAlgorithm(), provider).generatePublic(
new X509EncodedKeySpec(input.getEncoded()));
return publicKey;
} else if (input instanceof ECPrivateKey
&& "iaik.security.ec.common.ECPrivateKey".equals(input.getClass().getName())) {
//convert Key to BouncyCastle KeyImplemenation because there is an
//incompatibility with IAIK EC Keys and JWS signature-creation implementation
Key privateKey = KeyFactory.getInstance(
input.getAlgorithm(), provider).generatePrivate(
new PKCS8EncodedKeySpec(input.getEncoded()));
return privateKey;
}
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
log.warn("Can NOT convert {} to {}. The verification may FAIL.",
input.getClass().getName(), PublicKey.class.getName(), e);
}
return input;
}
/**
* Select signature algorithm for a given credential.
*
* @param key {@link X509Credential} that will be used for
* key operations
* @param rsaSigAlgorithm RSA based algorithm that should be used in case
* of RSA credential
* @param ecSigAlgorithm EC based algorithm that should be used in case
* of RSA credential
* @param friendlyNameForLogging KeyStore friendlyName for logging purposes
* @return either the RSA based algorithm or the EC based algorithm
* @throws EaafKeyUsageException In case of an unsupported private-key type
*/
private static String getKeyOperationAlgorithmFromCredential(Key key,
String rsaSigAlgorithm, String ecSigAlgorithm, String friendlyNameForLogging)
throws EaafKeyUsageException {
if (key instanceof RSAPrivateKey) {
return rsaSigAlgorithm;
} else if (key instanceof ECPrivateKey) {
return ecSigAlgorithm;
} else {
log.warn("Could NOT select the cryptographic algorithm from Private-Key type");
throw new EaafKeyUsageException(EaafKeyUsageException.ERROR_CODE_01,
friendlyNameForLogging,
"Can not select cryptographic algorithm");
}
}
private JoseUtils() {
}
@Getter
@AllArgsConstructor
public static class JwsResult {
final boolean valid;
final String payLoad;
final Headers fullJoseHeader;
final List x5cCerts;
}
}