package at.gv.egiz.eaaf.core.impl.utils; import java.io.IOException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyStore; import java.security.MessageDigest; 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.Date; 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 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 without signing certificate validation. * * @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 { return validateSignature(serializedContent, trustedCerts, constraints, false); } /** * 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 * @param needValidateSigningCertificate If true then the signing * certificate itself must be valid * @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, final boolean needValidateSigningCertificate) throws JoseException, IOException { final JsonWebSignature jws = new JsonWebSignature(); // set payload jws.setCompactSerialization(serializedContent); // set security constrains jws.setAlgorithmConstraints(constraints); // load signinc certs Key selectedKey = selectSigningKey(jws, trustedCerts); // validate signing-certificate constrains if (needValidateSigningCertificate) { validateSigningCertificate(selectedKey, trustedCerts); } // 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(), jws.getCertificateChainHeaderValue()); } /** * 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 static void validateSigningCertificate(Key selectedKey, List trustedCerts) throws JoseException { X509Certificate signingCert = trustedCerts.stream() .filter(el -> MessageDigest.isEqual(el.getPublicKey().getEncoded(), selectedKey.getEncoded())) .findFirst() .get(); Date now = new Date(); if (now.before(signingCert.getNotBefore()) || now.after(signingCert.getNotAfter())) { log.error("JOSE signing-certificate is not valid. Now: {}, Before: {}, After: {}", now, signingCert.getNotBefore(), signingCert.getNotAfter()); throw new JoseException("JOSE signing-certificate is not in validity periode"); } else { log.debug("JOSE signing-certificate is in validity periode"); } } private static Key selectSigningKey(JsonWebSignature jws, List trustedCerts) throws JoseException { 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))) { return 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); return x509VerificationKeyResolver.resolveKey(jws, Collections.emptyList()); } else { throw new JoseException("JWS contains NO signature certificate or NO certificate fingerprint"); } throw new JoseException("Can NOT select verification key for JWS. Signature verification FAILED"); } private JoseUtils() { } @Getter @AllArgsConstructor public static class JwsResult { final boolean valid; final String payLoad; final Headers fullJoseHeader; final List x5cCerts; } }