org.opensaml.xmlsec.algorithm.AlgorithmRegistry Maven / Gradle / Ivy
/*
* Licensed to the University Corporation for Advanced Internet Development,
* Inc. (UCAID) under one or more contributor license agreements. See the
* NOTICE file distributed with this work for additional information regarding
* copyright ownership. The UCAID licenses this file to You under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.opensaml.xmlsec.algorithm;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
import org.opensaml.xmlsec.algorithm.AlgorithmDescriptor.AlgorithmType;
import org.opensaml.xmlsec.encryption.support.EncryptionConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.MoreObjects;
/**
* A registry of {@link AlgorithmDescriptor} instances, to support various use cases for working with algorithm URIs.
*/
public class AlgorithmRegistry {
/** Logger. */
private Logger log = LoggerFactory.getLogger(AlgorithmRegistry.class);
/** Map of registered algorithm descriptors. */
private Map descriptors;
/** Index of registered AlgorithmType to algorithm URI. */
private Map> types;
/** Set containing algorithms which are supported by the runtime environment. */
private Set runtimeSupported;
/** Index of digest type to AlgorithmDescriptor. */
private Map digestAlgorithms;
/** Index of (KeyType,DigestType) to AlgorithmDescriptor. */
private Map signatureAlgorithms;
/** Constructor. */
public AlgorithmRegistry() {
descriptors = new HashMap<>();
types = new HashMap<>();
runtimeSupported = new HashSet<>();
digestAlgorithms = new HashMap<>();
signatureAlgorithms = new HashMap<>();
}
/**
* Get the algorithm descriptor instance associated with the specified algorithm URI.
* @param algorithmURI the algorithm URI to resolve
*
* @return the resolved algorithm descriptor or null
*/
@Nullable public AlgorithmDescriptor get(@Nullable final String algorithmURI) {
final String trimmed = StringSupport.trimOrNull(algorithmURI);
if (trimmed == null) {
return null;
}
return descriptors.get(trimmed);
}
/**
* Retrieve indication of whether the runtime environment supports the algorithm.
*
*
* This evaluation is performed dynamically when the algorithm is registered.
*
*
* @param algorithmURI the algorithm URI to evaluate
*
* @return true if the algorithm is supported by the current runtime environment, false otherwise
*/
public boolean isRuntimeSupported(@Nullable final String algorithmURI) {
final String trimmed = StringSupport.trimOrNull(algorithmURI);
if (trimmed == null) {
log.debug("Runtime support failed, algorithm URI was null or empty");
return false;
}
final boolean supported = runtimeSupported.contains(trimmed);
log.debug("Runtime support eval for algorithm URI '{}': {}", trimmed, supported ? "supported" : "unsupported");
return supported;
}
/**
* Clear all registered algorithms.
*/
public void clear() {
descriptors.clear();
runtimeSupported.clear();
digestAlgorithms.clear();
signatureAlgorithms.clear();
}
/**
* Register an algorithm.
*
* @param descriptor the algorithm
*/
public void register(@Nonnull final AlgorithmDescriptor descriptor) {
Constraint.isNotNull(descriptor, "AlgorithmDescriptor was null");
log.debug("Registering algorithm descriptor with URI: {}", descriptor.getURI());
final AlgorithmDescriptor old = descriptors.get(descriptor.getURI());
if (old != null) {
log.debug("Registry contained existing descriptor with URI, removing old instance and re-registering: {}",
descriptor.getURI());
deindex(old);
deregister(old);
}
descriptors.put(descriptor.getURI(), descriptor);
index(descriptor);
}
/**
* Deregister an algorithm.
*
* @param descriptor the algorithm
*/
public void deregister(@Nonnull final AlgorithmDescriptor descriptor) {
Constraint.isNotNull(descriptor, "AlgorithmDescriptor was null");
if (descriptors.containsKey(descriptor.getURI())) {
deindex(descriptor);
descriptors.remove(descriptor.getURI());
} else {
log.debug("Registry did not contain descriptor with URI, nothing to do: {}", descriptor.getURI());
}
}
/**
* Deregister an algorithm.
*
* @param uri the algorithm URI
*/
public void deregister(@Nonnull final String uri) {
Constraint.isNotNull(uri, "AlgorithmDescriptor URI was null");
final AlgorithmDescriptor descriptor = get(uri);
if (descriptor != null) {
deregister(descriptor);
}
}
/**
* Lookup a digest method algorithm descriptor by the JCA digest method ID.
*
* @param digestMethod the JCA digest method ID.
*
* @return the algorithm descriptor, or null
*/
@Nullable public DigestAlgorithm getDigestAlgorithm(@Nonnull final String digestMethod) {
Constraint.isNotNull(digestMethod, "Digest method was null");
return digestAlgorithms.get(digestMethod);
}
/**
* Lookup a signature algorithm descriptor by the JCA key algorithm and digest method IDs.
*
* @param keyType the JCA key algorithm ID.
* @param digestMethod the JCA digest method ID.
*
* @return the algorithm descriptor, or null
*/
@Nullable public SignatureAlgorithm getSignatureAlgorithm(@Nonnull final String keyType,
@Nonnull final String digestMethod) {
Constraint.isNotNull(keyType, "Key type was null");
Constraint.isNotNull(digestMethod, "Digest type was null");
return signatureAlgorithms.get(new SignatureAlgorithmIndex(keyType, digestMethod));
}
/**
* Get the set of algorithm URIs registered for the given type.
*
* @param type the algorithm type
*
* @return the set of URIs for the given type, may be empty
*/
@Nonnull @NonnullElements @Unmodifiable @NotLive
public Set getRegisteredURIsByType(@Nonnull final AlgorithmType type) {
Constraint.isNotNull(type, "AlgorithmType was null");
final Set byType = types.get(type);
if (byType != null) {
return Set.copyOf(byType);
}
return Collections.emptySet();
}
/**
* Get the set of {@link AlgorithmDescriptor} registered for the given type.
*
* @param type the algorithm type
*
* @return the set of descriptors for the given type, may be empty
*/
@Nonnull @NonnullElements @Unmodifiable @NotLive
public Set getRegisteredByType(@Nonnull final AlgorithmType type) {
return getRegisteredURIsByType(type).stream()
.map(this::get)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableSet());
}
/**
* Add the algorithm descriptor to the indexes which support the various lookup methods
* available via the registry's API.
*
* @param descriptor the algorithm
*/
private void index(final AlgorithmDescriptor descriptor) {
Set byType = types.get(descriptor.getType());
if (byType == null) {
byType = new HashSet<>();
types.put(descriptor.getType(), byType);
}
byType.add(descriptor.getURI());
if (checkRuntimeSupports(descriptor)) {
runtimeSupported.add(descriptor.getURI());
} else {
log.info("Algorithm failed runtime support check, will not be usable: {}", descriptor.getURI());
// Just for good measure, for case where environment has changed
// and algorithm is being re-registered.
runtimeSupported.remove(descriptor.getURI());
}
if (descriptor instanceof DigestAlgorithm) {
final DigestAlgorithm digestAlgorithm = (DigestAlgorithm) descriptor;
digestAlgorithms.put(digestAlgorithm.getJCAAlgorithmID(), digestAlgorithm);
}
if (descriptor instanceof SignatureAlgorithm) {
final SignatureAlgorithm sigAlg = (SignatureAlgorithm) descriptor;
signatureAlgorithms.put(new SignatureAlgorithmIndex(sigAlg.getKey(), sigAlg.getDigest()), sigAlg);
}
}
/**
* Remove the algorithm descriptor from the indexes which support the various lookup methods
* available via the registry's API.
*
* @param descriptor the algorithm
*/
private void deindex(final AlgorithmDescriptor descriptor) {
final Set byType = types.get(descriptor.getType());
if (byType != null) {
byType.remove(descriptor.getURI());
}
runtimeSupported.remove(descriptor.getURI());
if (descriptor instanceof DigestAlgorithm) {
final DigestAlgorithm digestAlgorithm = (DigestAlgorithm) descriptor;
digestAlgorithms.remove(digestAlgorithm.getJCAAlgorithmID());
}
if (descriptor instanceof SignatureAlgorithm) {
final SignatureAlgorithm sigAlg = (SignatureAlgorithm) descriptor;
signatureAlgorithms.remove(new SignatureAlgorithmIndex(sigAlg.getKey(), sigAlg.getDigest()));
}
}
/**
* Evaluate whether the algorithm is supported by the current runtime environment.
*
* @param descriptor the algorithm
*
* @return true if runtime supports the algorithm, false otherwise
*/
// Checkstyle: CyclomaticComplexity OFF
private boolean checkRuntimeSupports(final AlgorithmDescriptor descriptor) {
try {
switch(descriptor.getType()) {
case BlockEncryption:
case KeyTransport:
case SymmetricKeyWrap:
Cipher.getInstance(descriptor.getJCAAlgorithmID());
if (!checkCipherSupportedKeyLength(descriptor)) {
return false;
}
break;
case Signature:
Signature.getInstance(descriptor.getJCAAlgorithmID());
break;
case Mac:
Mac.getInstance(descriptor.getJCAAlgorithmID());
break;
case MessageDigest:
MessageDigest.getInstance(descriptor.getJCAAlgorithmID());
break;
default:
log.info("Saw unknown AlgorithmDescriptor type, failing runtime support check: {}",
descriptor.getClass().getName());
}
} catch (final NoSuchAlgorithmException | NoSuchPaddingException e) {
if (!checkSpecialCasesRuntimeSupport(descriptor)) {
log.debug(String.format("AlgorithmDescriptor failed runtime support check: %s",
descriptor.getURI()), e);
return false;
}
} catch (final Throwable t) {
log.error("Fatal error evaluating algorithm runtime support", t);
return false;
}
return true;
}
// Checkstyle: CyclomaticComplexity ON
/**
* Check if the key length of the specified {@link Cipher}-based algorithm, if known, is
* supported by the current runtime.
*
* @param descriptor the algorithm
* @return true if key length supported, false otherwise
* @throws NoSuchAlgorithmException if the associated JCA algorithm is not supported by the runtime
*/
private boolean checkCipherSupportedKeyLength(final AlgorithmDescriptor descriptor)
throws NoSuchAlgorithmException {
if (descriptor instanceof KeyLengthSpecifiedAlgorithm) {
final int algoLength = ((KeyLengthSpecifiedAlgorithm)descriptor).getKeyLength();
final int cipherMaxLength = Cipher.getMaxAllowedKeyLength(descriptor.getJCAAlgorithmID());
if (algoLength > cipherMaxLength) {
log.info("Cipher algorithm '{}' is not supported, its key length {} exceeds Cipher max key length {}",
descriptor.getURI(), algoLength, cipherMaxLength);
return false;
}
}
return true;
}
/**
* Check for special cases of runtime support which failed the initial simple service class load check.
*
* @param descriptor the algorithm
*
* @return true if algorithm is supported by the runtime environment, false otherwise
*/
private boolean checkSpecialCasesRuntimeSupport(final AlgorithmDescriptor descriptor) {
log.trace("Checking runtime support failure for special cases: {}", descriptor.getURI());
try {
// Per Santuario XMLCipher: Some JDKs don't support RSA/ECB/OAEPPadding.
// So check specifically for OAEPPadding with explicit SHA-1 digest and MGF1.
if (EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSAOAEP.equals(descriptor.getURI())) {
Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
log.trace("RSA OAEP algorithm passed as special case with OAEPWithSHA1AndMGF1Padding");
return true;
}
} catch (final NoSuchAlgorithmException | NoSuchPaddingException e) {
log.trace("Special case eval for algorithm failed with exception", e);
}
log.trace("Algorithm was not supported by any special cases: {}", descriptor.getURI());
return false;
}
/**
* Class used as index key for signature algorithm lookup.
*/
protected class SignatureAlgorithmIndex {
/** Key type. */
private String key;
/** Digest type. */
private String digest;
/**
* Constructor.
*
* @param keyType the key type
* @param digestType the digest type
*/
public SignatureAlgorithmIndex(@Nonnull final String keyType, @Nonnull final String digestType) {
key = StringSupport.trim(keyType);
digest = StringSupport.trim(digestType);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
int result = 17;
result = 37*result + key.hashCode();
result = 37*result + digest.hashCode();
return result;
}
/** {@inheritDoc} */
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof SignatureAlgorithmIndex) {
final SignatureAlgorithmIndex other = (SignatureAlgorithmIndex) obj;
return Objects.equals(key, other.key) && Objects.equals(digest, other.digest);
}
return false;
}
/** {@inheritDoc} */
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("Key", key)
.add("Digest", digest).toString();
}
}
}