[java-opensaml] 15/15: OSJ-118: Implement PublicKey <-> ECKeyValue in KeyInfoSupport
Brent Putman
putmanb at georgetown.edu
Thu Jan 21 21:33:35 UTC 2021
This is an automated email from the git hooks/post-receive script.
putmanb pushed a commit to branch dev/OSJ-82
in repository java-opensaml.
View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=c56b4119a883e89c2eaaaa28fca121d14b760fe2
commit c56b4119a883e89c2eaaaa28fca121d14b760fe2
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Mon Jan 18 20:44:52 2021 -0500
OSJ-118: Implement PublicKey <-> ECKeyValue in KeyInfoSupport
---
opensaml-security-api/pom.xml | 8 +
.../GlobalNamedCurveRegistryInitializer.java | 67 ++++++
.../org/opensaml/security/config/package-info.java | 19 ++
.../org/opensaml/security/crypto/ec/ECSupport.java | 224 ++++++++++++++++++++-
.../crypto/ec/EnhancedECParameterSpec.java | 82 ++++++++
.../opensaml/security/crypto/ec/NamedCurve.java | 68 +++++++
.../security/crypto/ec/NamedCurveRegistry.java | 222 ++++++++++++++++++++
.../crypto/ec/curves/AbstractNamedCurve.java | 120 +++++++++++
.../security/crypto/ec/curves/Secp256r1.java | 35 ++++
.../security/crypto/ec/curves/Secp384r1.java | 35 ++++
.../security/crypto/ec/curves/Secp521r1.java | 35 ++++
.../security/crypto/ec/curves/package-info.java | 19 ++
.../opensaml/security/crypto/ec/package-info.java | 19 ++
.../services/org.opensaml.core.config.Initializer | 1 +
.../org.opensaml.security.crypto.ec.NamedCurve | 3 +
.../security/crypto/ec/BaseNamedCurveTest.java | 37 ++++
.../opensaml/security/crypto/ec/ECSupportTest.java | 56 +++++-
.../crypto/ec/EnhancedECParameterSpecTest.java | 69 +++++++
.../security/crypto/ec/NamedCurveRegistryTest.java | 163 +++++++++++++++
.../security/crypto/ec/curves/NamedCurvesTest.java | 75 +++++++
.../opensaml/xmlsec/keyinfo/KeyInfoSupport.java | 109 +++++++++-
.../impl/KeyAgreementKeyInfoGeneratorFactory.java | 11 +-
.../keyinfo/impl/provider/ECKeyValueProvider.java | 115 +++++++++++
.../xmlsec/keyinfo/impl/ECKeyValueTest.java | 94 +++++++++
.../impl/KeyAgreementKeyInfoGeneratorTest.java | 28 ++-
.../xmlsec/keyinfo/tests/KeyInfoSupportTest.java | 208 ++++++++++++++++++-
.../opensaml/xmlsec/keyinfo/impl/ECKeyValue.xml | 11 +
27 files changed, 1882 insertions(+), 51 deletions(-)
diff --git a/opensaml-security-api/pom.xml b/opensaml-security-api/pom.xml
index 2163bb9d9..cc5dcd5b4 100644
--- a/opensaml-security-api/pom.xml
+++ b/opensaml-security-api/pom.xml
@@ -77,6 +77,14 @@
<!-- Runtime Dependencies -->
<!-- Test Dependencies -->
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>opensaml-core</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
<dependency>
<groupId>${spring.groupId}</groupId>
<artifactId>spring-beans</artifactId>
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/config/GlobalNamedCurveRegistryInitializer.java b/opensaml-security-api/src/main/java/org/opensaml/security/config/GlobalNamedCurveRegistryInitializer.java
new file mode 100644
index 000000000..e4eac5caa
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/config/GlobalNamedCurveRegistryInitializer.java
@@ -0,0 +1,67 @@
+/*
+ * 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.security.config;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+import org.opensaml.core.config.ConfigurationService;
+import org.opensaml.core.config.InitializationException;
+import org.opensaml.core.config.Initializer;
+import org.opensaml.security.crypto.ec.NamedCurve;
+import org.opensaml.security.crypto.ec.NamedCurveRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.InitializableComponent;
+
+/**
+ * OpenSAML {@link Initializer} implementation for {@link NamedCurveRegistry}.
+ */
+public class GlobalNamedCurveRegistryInitializer implements Initializer {
+
+ /** Logger. */
+ private Logger log = LoggerFactory.getLogger(GlobalNamedCurveRegistryInitializer.class);
+
+ /** {@inheritDoc} */
+ public void init() throws InitializationException {
+ final NamedCurveRegistry registry = new NamedCurveRegistry();
+
+ final ServiceLoader<NamedCurve> curvesLoader = ServiceLoader.load(NamedCurve.class);
+ final Iterator<NamedCurve> iter = curvesLoader.iterator();
+ while (iter.hasNext()) {
+ final NamedCurve curve = iter.next();
+ try {
+ if (InitializableComponent.class.isInstance(curve)) {
+ InitializableComponent.class.cast(curve).initialize();
+ }
+ } catch (final ComponentInitializationException e) {
+ log.warn("Error initing NamedCurve with name '{}', OID '{}', URI '{}': {}",
+ curve.getName(), curve.getObjectIdentifier(), curve.getURI(), curve.getClass().getName());
+ continue;
+ }
+ log.debug("Registering NamedCurve with name '{}', OID '{}' and name '{}': {}'",
+ curve.getName(), curve.getObjectIdentifier(), curve.getURI(), curve.getClass().getName());
+ registry.register(curve);
+ }
+
+ ConfigurationService.register(NamedCurveRegistry.class, registry);
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/config/package-info.java b/opensaml-security-api/src/main/java/org/opensaml/security/config/package-info.java
new file mode 100644
index 000000000..51078b438
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/config/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** OpenSAML configuration classes. */
+package org.opensaml.security.config;
\ No newline at end of file
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/ECSupport.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/ECSupport.java
index 4121f72ba..30c7fbebb 100644
--- a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/ECSupport.java
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/ECSupport.java
@@ -17,33 +17,50 @@
package org.opensaml.security.crypto.ec;
+import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
+import java.security.KeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import java.util.Arrays;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.KeyAgreement;
+import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
+import org.opensaml.core.config.ConfigurationService;
import org.opensaml.security.crypto.JCAConstants;
import org.opensaml.security.crypto.KeySupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
/**
* Cryptography support related to Elliptic Curve.
*/
public final class ECSupport {
+ /** Logger. */
+ private static final Logger LOG = LoggerFactory.getLogger(ECSupport.class);
+
/** Constructor. */
private ECSupport() { }
/**
* Perform ECDH key agreement between the given public and private keys.
*
- * @param publicKeyKey the public key
+ * @param publicKey the public key
* @param privateKey the private key
* @param provider the optional security provider to use
*
@@ -53,9 +70,11 @@ public final class ECSupport {
* @throws NoSuchProviderException
* @throws InvalidKeyException
*/
- public static byte[] performKeyAgreement(@Nonnull final ECPublicKey publicKeyKey,
+ public static byte[] performKeyAgreement(@Nonnull final ECPublicKey publicKey,
@Nonnull final ECPrivateKey privateKey, @Nullable final String provider)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException {
+ Constraint.isNotNull(publicKey, "ECPublicKey was null");
+ Constraint.isNotNull(privateKey, "ECPrivateKey was null");
KeyAgreement keyAgreement = null;
if (provider != null) {
@@ -65,7 +84,7 @@ public final class ECSupport {
}
keyAgreement.init(privateKey);
- keyAgreement.doPhase(publicKeyKey, true);
+ keyAgreement.doPhase(publicKey, true);
return keyAgreement.generateSecret();
}
@@ -84,7 +103,204 @@ public final class ECSupport {
public static KeyPair generateCompatibleKeyPair(@Nonnull final ECPublicKey publicKey,
@Nullable final String provider)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
+ Constraint.isNotNull(publicKey, "ECPublicKey was null");
+
return KeySupport.generateKeyPair(JCAConstants.KEY_ALGO_EC, publicKey.getParams(), provider);
}
-}
+ /**
+ * Get the global {@link NamedCurveRegistry} instance.
+ *
+ * @return the global named curve registry, or null if nothing registered
+ */
+ @Nullable public static NamedCurveRegistry getGlobalNamedCurveRegistry() {
+ return ConfigurationService.get(NamedCurveRegistry.class);
+ }
+
+ /**
+ * Get the {@link NamedCurve} for the specified {@link ECPublicKey}.
+ *
+ * @param publicKey the {@link ECPublicKey}
+ *
+ * @return the {@@link NamedCurve} instance, or null if can not be determined,
+ * possibly because the key's domain parameters do not correspond to a named curve
+ */
+ @Nullable public static NamedCurve getNamedCurve(@Nonnull final ECPublicKey publicKey) {
+ Constraint.isNotNull(publicKey, "ECPublicKey was null");
+
+ final NamedCurveRegistry registry = getGlobalNamedCurveRegistry();
+ if (registry == null) {
+ LOG.warn("No NamedCurveRegistry is configured");
+ return null;
+ }
+ return registry.getByParameterSpec(publicKey.getParams());
+ }
+
+ /**
+ * Get the {@link NamedCurve} for the specified URI.
+ *
+ * @param uri the URI
+ *
+ * @return the {@@link NamedCurve} instance, or null if can not be determined,
+ */
+ @Nullable public static NamedCurve getNamedCurve(@Nonnull final String uri) {
+ Constraint.isNotNull(uri, "NamedCurve URI was null");
+
+ final NamedCurveRegistry registry = getGlobalNamedCurveRegistry();
+ if (registry == null) {
+ LOG.warn("No NamedCurveRegistry is configured");
+ return null;
+ }
+ return registry.getByURI(uri);
+ }
+
+ /**
+ * Get the URI of the named curve for the specified {@link ECPublicKey}.
+ *
+ * @param publicKey the {@link ECPublicKey}
+ *
+ * @return the URI or null if can not be determined, possibly because is not a named curve
+ */
+ @Nullable public static String getNamedCurveURI(@Nonnull final ECPublicKey publicKey) {
+ Constraint.isNotNull(publicKey, "ECPublicKey was null");
+
+ //TODO implement fallback to do nasty parsing of the ASN.1 encoded form for the OID?
+ final NamedCurve namedCurve = getNamedCurve(publicKey);
+ if (namedCurve == null) {
+ LOG.warn("Could not resolve NamedCurve for ECPublicKey");
+ return null;
+ }
+ return namedCurve.getURI();
+ }
+
+ /**
+ * Get an {@link ECParameterSpec} instance which corresponds to the specified named curve URI.
+ *
+ * @param uri the URI of the named curve
+ *
+ * @return the {@link ECParameterSpec} instance
+ *
+ * @throws KeyException
+ */
+ @Nullable public static ECParameterSpec getParameterSpecForURI(@Nonnull final String uri) {
+ Constraint.isNotNull(uri, "NamedCurve URI was null");
+
+ //TODO implement fallback to use BCNamedCurveTable to lookup up OID -> param spec?
+ final NamedCurve namedCurve = getNamedCurve(uri);
+ if (namedCurve == null) {
+ LOG.warn("Could not resolve NamedCurve for URI: {}", uri);
+ return null;
+ }
+ return namedCurve.getParameterSpec();
+ }
+
+ /**
+ * Decode the {@link ECPoint} from the byte representation.
+ *
+ * <p>
+ * Only uncompressed point types (0x04) are supported.
+ * </p>
+ *
+ * @param data the EC point byte representation
+ * @param curve the {@link EllipticCurve}
+ *
+ * @return the {@link ECPoint}
+ *
+ * @throws KeyException
+ */
+ @Nonnull public static ECPoint decodeECPoint(@Nonnull final byte[] data, @Nonnull final EllipticCurve curve)
+ throws KeyException {
+ Constraint.isNotNull(data, "ECPoint byte array was null");
+ Constraint.isNotNull(curve, "EllipticCurve was null");
+
+ // This implementation borrowed from Santuario 2.2.1 (unfortunately private static methods)
+ // See: org.apache.xml.security.keys.content.keyvalues.ECKeyValue#decodePoint(...)
+
+ if (data.length == 0 || data[0] != 4) {
+ throw new KeyException("Only uncompressed point format supported");
+ }
+
+ // Per ANSI X9.62, an encoded point is a 1 byte type followed by
+ // ceiling(LOG base 2 field-size / 8) bytes of x and the same of y.
+ final int n = (data.length - 1) / 2;
+ if (n != (curve.getField().getFieldSize() + 7) >> 3) {
+ throw new KeyException("Point does not match field size");
+ }
+
+ final byte[] xb = Arrays.copyOfRange(data, 1, 1 + n);
+ final byte[] yb = Arrays.copyOfRange(data, n + 1, n + 1 + n);
+
+ return new ECPoint(new BigInteger(1, xb), new BigInteger(1, yb));
+ }
+
+ /**
+ * Encode the uncompressed byte representation of the specified {@link ECPoint}.
+ *
+ * @param point the {@link ECPoint}
+ * @param curve the {@link EllipticCurve}
+ *
+ * @return the uncompressed byte representation
+ */
+ @Nonnull public static byte[] encodeECPointUncompressed(@Nonnull final ECPoint point,
+ @Nonnull final EllipticCurve curve) {
+ Constraint.isNotNull(point, "ECPoint was null");
+ Constraint.isNotNull(curve, "EllipticCurve was null");
+
+ // This implementation borrowed from Santuario 2.2.1 (unfortunately private static methods)
+ // See: org.apache.xml.security.keys.content.keyvalues.ECKeyValue#encodePoint(...)
+
+ // get field size in bytes (rounding up)
+ final int n = (curve.getField().getFieldSize() + 7) >> 3;
+ final byte[] xb = trimZeroes(point.getAffineX().toByteArray());
+ final byte[] yb = trimZeroes(point.getAffineY().toByteArray());
+ if (xb.length > n || yb.length > n) {
+ throw new IllegalArgumentException("Point coordinates do not match field size");
+ }
+ final byte[] b = new byte[1 + (n << 1)];
+ // 0x04 indicates the uncompressed type
+ b[0] = 4;
+ System.arraycopy(xb, 0, b, n - xb.length + 1, xb.length);
+ System.arraycopy(yb, 0, b, b.length - yb.length, yb.length);
+ return b;
+ }
+
+ /**
+ * Trim leading zero bytes from the byte array.
+ *
+ * @param b the byte array
+ * @return the byte array without leading zero bytes
+ */
+ private static byte[] trimZeroes(@Nonnull final byte[] b) {
+ Constraint.isNotNull(b, "byte[] data was null");
+
+ int i = 0;
+ while (i < b.length - 1 && b[i] == 0) {
+ i++;
+ }
+ if (i == 0) {
+ return b;
+ }
+ return Arrays.copyOfRange(b, i, b.length);
+ }
+
+ /**
+ * Convert a Bouncy Castle {@link ECNamedCurveParameterSpec}, such as obtained from the {@link ECNamedCurveTable},
+ * to a standard JCA {@link ECParameterSpec}.
+ *
+ * @param bcSpec the Bouncy Castle parameter spec instance
+ *
+ * @return the standard parameter spec instance
+ */
+ @Nullable public static ECParameterSpec convert(@Nullable final ECNamedCurveParameterSpec bcSpec) {
+ if (bcSpec == null) {
+ return null;
+ }
+
+ return new ECParameterSpec(
+ EC5Util.convertCurve(bcSpec.getCurve(), bcSpec.getSeed()),
+ EC5Util.convertPoint(bcSpec.getG()),
+ bcSpec.getN(),
+ bcSpec.getH().intValue());
+ }
+
+}
\ No newline at end of file
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/EnhancedECParameterSpec.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/EnhancedECParameterSpec.java
new file mode 100644
index 000000000..9c4030230
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/EnhancedECParameterSpec.java
@@ -0,0 +1,82 @@
+/*
+ * 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.security.crypto.ec;
+
+import java.security.spec.ECParameterSpec;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * A specialized subclass of {@link ECParameterSpec} which wraps an existing instance and implements
+ * {@link #hashCode()} and {@link #equals(Object)} so that it may be used directly in hashtable-based collections,
+ * as well as simplifying equality comparisons between 2 instances.
+ */
+public class EnhancedECParameterSpec extends ECParameterSpec {
+
+ /** The original instance. */
+ private final ECParameterSpec original;
+
+ /**
+ * Constructor.
+ *
+ * @param spec the parameter spec instance to wrap
+ */
+ public EnhancedECParameterSpec(@Nonnull final ECParameterSpec spec) {
+ super(spec.getCurve(), spec.getGenerator(), spec.getOrder(), spec.getCofactor());
+ original = spec;
+ }
+
+ /**
+ * Get the original instance passed to the constructor.
+ *
+ * @return the original instance
+ */
+ @Nonnull public ECParameterSpec getOriginal() {
+ return original;
+ }
+
+ /** {@inheritDoc} */
+ public int hashCode() {
+ return Objects.hash(getCurve(), getGenerator(), getOrder(), getCofactor());
+ }
+
+ /** {@inheritDoc} */
+ public boolean equals(@Nullable final Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (obj instanceof ECParameterSpec) {
+ final ECParameterSpec other = (ECParameterSpec) obj;
+ // Copying Santuario's logic here. It seems curve's ECField is an interface and the impls
+ // don't obviously take into account the field size equality. This field size compare is maybe
+ // redundant with ECField.equals(), but eval it explicitly to be safe.
+ // Comparing 2 int values isn't expensive.
+ return this.getCurve().getField().getFieldSize() == other.getCurve().getField().getFieldSize()
+ && this.getCurve().equals(other.getCurve())
+ && this.getGenerator().equals(other.getGenerator())
+ && this.getOrder().equals(other.getOrder())
+ && this.getCofactor() == other.getCofactor();
+ }
+
+ return false;
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/NamedCurve.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/NamedCurve.java
new file mode 100644
index 000000000..a1a5e093c
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/NamedCurve.java
@@ -0,0 +1,68 @@
+/*
+ * 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.security.crypto.ec;
+
+import java.security.spec.ECParameterSpec;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Descriptor for an EC named curve.
+ */
+public interface NamedCurve {
+
+ /**
+ * Get the curve's object identifier (OID).
+ *
+ * @return the OID
+ */
+ @Nonnull String getObjectIdentifier();
+
+ /**
+ * Get the curve's URI.
+ *
+ * @return the URI
+ */
+ @Nonnull default String getURI() {
+ return "urn:oid:" + getObjectIdentifier();
+ }
+
+ /**
+ * Get the curve's canonical name by which it is known to the Java Cryptography Architecture (JCA).
+ *
+ * @return the name
+ */
+ @Nonnull String getName();
+
+ /**
+ * Get the curve's {@link ECParameterSpec}.
+ *
+ * @return the parameter spec instance
+ */
+ @Nonnull ECParameterSpec getParameterSpec();
+
+ /**
+ * Get the length of a key based on the curve.
+ *
+ * @return the key length, in bits
+ */
+ @Nonnull default Integer getKeyLength() {
+ return getParameterSpec().getCurve().getField().getFieldSize();
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/NamedCurveRegistry.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/NamedCurveRegistry.java
new file mode 100644
index 000000000..ce555aac6
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/NamedCurveRegistry.java
@@ -0,0 +1,222 @@
+/*
+ * 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.security.crypto.ec;
+
+import java.security.spec.ECParameterSpec;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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;
+
+/**
+ * A registry of {@link NamedCurve} descriptors.
+ */
+public class NamedCurveRegistry {
+
+ /** Logger. */
+ private final Logger log = LoggerFactory.getLogger(NamedCurveRegistry.class);
+
+ /** Index by OID. */
+ private Map<String, NamedCurve> byOID;
+
+ /** Index by URI. */
+ private Map<String, NamedCurve> byURI;
+
+ /** Index by name. */
+ private Map<String, NamedCurve> byName;
+
+ /** Index by {@link EnhancedECParameterSpec}. */
+ private Map<EnhancedECParameterSpec, NamedCurve> byParamSpec;
+
+
+ /**
+ * Constructor.
+ */
+ public NamedCurveRegistry() {
+ byOID = new HashMap<>();
+ byURI = new HashMap<>();
+ byName = new HashMap<>();
+ byParamSpec = new HashMap<>();
+ }
+
+ /**
+ * Register a curve.
+ *
+ * @param curve the curve to register
+ */
+ public void register(@Nonnull final NamedCurve curve) {
+ Constraint.isNotNull(curve, "NamedCurve was null in registration");
+
+ byOID.put(curve.getObjectIdentifier(), curve);
+ byURI.put(curve.getURI(), curve);
+ byName.put(curve.getName(), curve);
+ byParamSpec.put(new EnhancedECParameterSpec(curve.getParameterSpec()), curve);
+
+ log.debug("Registered NamedCurve: {}", curve);
+ }
+
+ /**
+ * Deregister a curve.
+ *
+ * @param curve the curve to deregister
+ */
+ public void deregister(@Nonnull final NamedCurve curve) {
+ Constraint.isNotNull(curve, "NamedCurve was null in deregistration");
+
+ byOID.remove(curve.getObjectIdentifier());
+ byURI.remove(curve.getURI());
+ byName.remove(curve.getName());
+ byParamSpec.remove(new EnhancedECParameterSpec(curve.getParameterSpec()));
+
+ log.debug("Deregistered NamedCurve: {}", curve);
+ }
+
+ /**
+ * Deregister a curve.
+ *
+ * @param oid the object identifier (OID) of the curve to deregister
+ */
+ public void deregisterByOID(@Nonnull final String oid) {
+ Constraint.isNotNull(oid, "OID was null in NamedCurve deregistration");
+ final NamedCurve curve = getByOID(oid);
+ if (curve != null) {
+ deregister(curve);
+ }
+ }
+
+ /**
+ * Deregister a curve.
+ *
+ * @param uri the URI
+ */
+ public void deregisterByURI(@Nonnull final String uri) {
+ Constraint.isNotNull(uri, "URI was null in NamedCurve deregistration");
+ final NamedCurve curve = getByURI(uri);
+ if (curve != null) {
+ deregister(curve);
+ }
+ }
+
+ /**
+ * Deregister a curve.
+ *
+ * @param name the curve name
+ */
+ public void deregisterByName(@Nonnull final String name) {
+ Constraint.isNotNull(name, "Name was null in NamedCurve deregistration");
+ final NamedCurve curve = getByName(name);
+ if (curve != null) {
+ deregister(curve);
+ }
+ }
+
+ /**
+ * Deregister a curve.
+ *
+ * @param spec the parameter spec instance
+ */
+ public void deregisterByParameterSpec(@Nonnull final ECParameterSpec spec) {
+ Constraint.isNotNull(spec, "ECParameterSpec was null in NamedCurve deregistration");
+ final NamedCurve curve = getByParameterSpec(spec);
+ if (curve != null) {
+ deregister(curve);
+ }
+ }
+
+ /**
+ * Clear all registered curves.
+ */
+ public void clear() {
+ byOID.clear();
+ byURI.clear();
+ byName.clear();
+ byParamSpec.clear();
+
+ log.debug("Cleared all registered NamedCurves");
+ }
+
+ /**
+ * Get a set of all the registered curves.
+ *
+ * @return the set of registered curves
+ */
+ @Nonnull @NonnullElements @Unmodifiable @NotLive
+ public Set<NamedCurve> getRegisteredCurves() {
+ return Set.copyOf(byOID.values());
+ }
+
+ /**
+ * Lookup a curve by object identifier (OID).
+ *
+ * @param oid the object identifier
+ *
+ * @return the {@link NamedCurve} instance, or null if no registered curve matched
+ */
+ @Nullable public NamedCurve getByOID(@Nonnull final String oid) {
+ Constraint.isNotNull(oid, "OID was null in NamedCurve lookup");
+ return byOID.get(StringSupport.trimOrNull(oid));
+ }
+
+ /**
+ * Lookup a curve by URI.
+ *
+ * @param uri the URI
+ *
+ * @return the {@link NamedCurve} instance, or null if no registered curve matched
+ */
+ @Nullable public NamedCurve getByURI(@Nonnull final String uri) {
+ Constraint.isNotNull(uri, "URI was null in NamedCurve lookup");
+ return byURI.get(StringSupport.trimOrNull(uri));
+ }
+
+ /**
+ * Lookup a curve by the canonical name by which it is known to the Java Cryptography Architecture (JCA).
+ *
+ * @param name the name
+ *
+ * @return the {@link NamedCurve} instance, or null if no registered curve matched
+ */
+ @Nullable public NamedCurve getByName(@Nonnull final String name) {
+ Constraint.isNotNull(name, "Name was null in NamedCurve lookup");
+ return byName.get(StringSupport.trimOrNull(name));
+ }
+
+ /**
+ * Lookup a curve by {@link ECParameterSpec}.
+ *
+ * @param spec the parameter spec instance
+ *
+ * @return the {@link NamedCurve} instance, or null if no registered curve matched
+ */
+ @Nullable public NamedCurve getByParameterSpec(@Nonnull final ECParameterSpec spec) {
+ Constraint.isNotNull(spec, "ECParameterSpec was null in NamedCurve lookup");
+ return byParamSpec.get(new EnhancedECParameterSpec(spec));
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/AbstractNamedCurve.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/AbstractNamedCurve.java
new file mode 100644
index 000000000..e36aa5105
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/AbstractNamedCurve.java
@@ -0,0 +1,120 @@
+/*
+ * 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.security.crypto.ec.curves;
+
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+
+import javax.annotation.Nullable;
+
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.opensaml.security.crypto.JCAConstants;
+import org.opensaml.security.crypto.KeySupport;
+import org.opensaml.security.crypto.ec.ECSupport;
+import org.opensaml.security.crypto.ec.NamedCurve;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+
+/**
+ * Abstract base class for implementations of {@link NamedCurve}.
+ */
+public abstract class AbstractNamedCurve extends AbstractInitializableComponent implements NamedCurve {
+
+ /** Logger. */
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ /** Instance of {@link ECParameterSpec} corresponding to the curve. */
+ @NonnullAfterInit private ECParameterSpec paramSpec;
+
+ /** {@inheritDoc} */
+ @NonnullAfterInit public ECParameterSpec getParameterSpec() {
+ ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+ return paramSpec;
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ paramSpec = buildParameterSpec();
+
+ // This should never happen for any correctly-specified named curve that we'd actually define...
+ if (paramSpec == null) {
+ throw new ComponentInitializationException("Could not init NamedCurve ECParameterSpec");
+ }
+ }
+
+ /**
+ * Build an instance of {@link ECParameterSpec} corresponding to this curve.
+ *
+ * <p>
+ * The default implementation here is that it first attempts to resolve the curve from
+ * Bouncy Castle's {@link ECNamedCurveTable}. If that is unsuccessful then it attempts
+ * a brute force approach by generating a key pair using a {@link ECGenParameterSpec} based
+ * on the curve's name from {@link #getName()}, returning the parameter instance from the
+ * resulting {@link ECPublicKey}.
+ * </p>
+ *
+ * @return the parameter spec instance, or null if can not be built
+ */
+ @Nullable protected ECParameterSpec buildParameterSpec() {
+ ECParameterSpec jcaSpec = ECSupport.convert(ECNamedCurveTable.getParameterSpec(getObjectIdentifier()));
+ if (jcaSpec != null) {
+ log.trace("Inited NamedCurve ECParameterSpec from BC curve table for name '{}', OID '{}'",
+ getName(), getObjectIdentifier());
+ return jcaSpec;
+ }
+
+ log.trace("Failed to init NamedCurve ECParameterSpec from BC named curve table, trying keypair generation");
+
+ try {
+ jcaSpec = ECPublicKey.class.cast(
+ KeySupport.generateKeyPair(JCAConstants.KEY_ALGO_EC, new ECGenParameterSpec(getName()), null)
+ .getPublic()).getParams();
+ log.trace("Inited NamedCurve ECParameterSpec via key pair generation for name '{}', OID '{}'",
+ getName(), getObjectIdentifier());
+ return jcaSpec;
+ } catch (final Exception e) {
+ log.warn("Error initing the NamedCurve ECParameterSpce via key pair generation with name: {}",
+ getName(), e);
+ }
+
+ log.warn("Failed to init NamedCurve ECParameterSpec from BC or key pair generation for name '{}', OID '{}'",
+ getName(), getObjectIdentifier());
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("name", getName())
+ .add("OID", getObjectIdentifier())
+ .toString();
+ }
+
+}
+
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp256r1.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp256r1.java
new file mode 100644
index 000000000..56baf1555
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp256r1.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.crypto.ec.curves;
+
+/**
+ * Descriptor for named curve 'secp256r1', OID: 1.2.840.10045.3.1.7.
+ */
+public class Secp256r1 extends AbstractNamedCurve {
+
+ /** {@inheritDoc} */
+ public String getObjectIdentifier() {
+ return "1.2.840.10045.3.1.7";
+ }
+
+ /** {@inheritDoc} */
+ public String getName() {
+ return "secp256r1";
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp384r1.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp384r1.java
new file mode 100644
index 000000000..fc5ce3af4
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp384r1.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.crypto.ec.curves;
+
+/**
+ * Descriptor for named curve 'secp384r1', OID: 1.3.132.0.34.
+ */
+public class Secp384r1 extends AbstractNamedCurve {
+
+ /** {@inheritDoc} */
+ public String getObjectIdentifier() {
+ return "1.3.132.0.34";
+ }
+
+ /** {@inheritDoc} */
+ public String getName() {
+ return "secp384r1";
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp521r1.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp521r1.java
new file mode 100644
index 000000000..8139e8da4
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/Secp521r1.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.crypto.ec.curves;
+
+/**
+ * Descriptor for named curve 'secp521r1', OID: 1.3.132.0.35.
+ */
+public class Secp521r1 extends AbstractNamedCurve {
+
+ /** {@inheritDoc} */
+ public String getObjectIdentifier() {
+ return "1.3.132.0.35";
+ }
+
+ /** {@inheritDoc} */
+ public String getName() {
+ return "secp521r1";
+ }
+
+}
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/package-info.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/package-info.java
new file mode 100644
index 000000000..4e8b82d3f
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/curves/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Implementations of {@link org.opensaml.security.crypto.ec.NamedCurve} used with Elliptic-Curve cryptography. */
+package org.opensaml.security.crypto.ec.curves;
\ No newline at end of file
diff --git a/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/package-info.java b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/package-info.java
new file mode 100644
index 000000000..9fee76eb3
--- /dev/null
+++ b/opensaml-security-api/src/main/java/org/opensaml/security/crypto/ec/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Support for Elliptic-Curve cryptography. */
+package org.opensaml.security.crypto.ec;
\ No newline at end of file
diff --git a/opensaml-security-api/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer b/opensaml-security-api/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer
new file mode 100644
index 000000000..20479a730
--- /dev/null
+++ b/opensaml-security-api/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer
@@ -0,0 +1 @@
+org.opensaml.security.config.GlobalNamedCurveRegistryInitializer
\ No newline at end of file
diff --git a/opensaml-security-api/src/main/resources/META-INF/services/org.opensaml.security.crypto.ec.NamedCurve b/opensaml-security-api/src/main/resources/META-INF/services/org.opensaml.security.crypto.ec.NamedCurve
new file mode 100644
index 000000000..80b2fa93c
--- /dev/null
+++ b/opensaml-security-api/src/main/resources/META-INF/services/org.opensaml.security.crypto.ec.NamedCurve
@@ -0,0 +1,3 @@
+org.opensaml.security.crypto.ec.curves.Secp256r1
+org.opensaml.security.crypto.ec.curves.Secp384r1
+org.opensaml.security.crypto.ec.curves.Secp521r1
\ No newline at end of file
diff --git a/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/BaseNamedCurveTest.java b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/BaseNamedCurveTest.java
new file mode 100644
index 000000000..be6b4a263
--- /dev/null
+++ b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/BaseNamedCurveTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.security.crypto.ec;
+
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.testng.annotations.DataProvider;
+
+/**
+ * This mostly exists as a single place to define the common set of named curves we want to test in various tests.
+ */
+public class BaseNamedCurveTest extends OpenSAMLInitBaseTestCase {
+
+ @DataProvider
+ public Object[][] namedCurves() {
+ return new Object[][] {
+ new Object[] {"secp256r1"},
+ new Object[] {"secp384r1"},
+ new Object[] {"secp521r1"},
+ };
+ }
+
+}
diff --git a/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/ECSupportTest.java b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/ECSupportTest.java
index bcff2cbb4..b7dc1cedd 100644
--- a/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/ECSupportTest.java
+++ b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/ECSupportTest.java
@@ -17,30 +17,25 @@
package org.opensaml.security.crypto.ec;
+import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import org.bouncycastle.jce.ECNamedCurveTable;
import org.opensaml.security.crypto.JCAConstants;
+import org.opensaml.security.crypto.KeySupport;
import org.testng.Assert;
-import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
*
*/
-public class ECSupportTest {
-
- @DataProvider
- public Object[][] namedCurves() {
- return new Object[][] {
- new Object[] {"secp256r1"},
- new Object[] {"secp384r1"},
- new Object[] {"secp521r1"},
- };
- }
+public class ECSupportTest extends BaseNamedCurveTest {
@Test(dataProvider="namedCurves")
public void generateCompatibleKeyPair(String namedCurve) throws Exception {
@@ -72,5 +67,44 @@ public class ECSupportTest {
byte[] secret = ECSupport.performKeyAgreement(publicKey, privateKey, null);
Assert.assertNotNull(secret);
}
+
+ @Test(dataProvider="namedCurves")
+ public void convertParameterSpec(String namedCurve) throws Exception {
+ ECParameterSpec control = ECPublicKey.class.cast(
+ KeySupport.generateKeyPair(JCAConstants.KEY_ALGO_EC, new ECGenParameterSpec(namedCurve), null).getPublic()).getParams();
+
+ ECParameterSpec target = ECSupport.convert(ECNamedCurveTable.getParameterSpec(namedCurve));
+ Assert.assertNotNull(target);
+
+ Assert.assertNotSame(target, control);
+
+ Assert.assertEquals(target.getCurve().getField().getFieldSize(), control.getCurve().getField().getFieldSize());
+ Assert.assertEquals(target.getCurve(), control.getCurve());
+ Assert.assertEquals(target.getGenerator(), control.getGenerator());
+ Assert.assertEquals(target.getOrder(), control.getOrder());
+ Assert.assertEquals(target.getCofactor(), control.getCofactor());
+ }
+ @Test(dataProvider="namedCurves")
+ public void encodeAndDecodeECPoint(String namedCurve) throws Exception {
+ ECParameterSpec spec = ECPublicKey.class.cast(
+ KeySupport.generateKeyPair(JCAConstants.KEY_ALGO_EC, new ECGenParameterSpec(namedCurve), null).getPublic()).getParams();
+
+ // Do this differently (and clearer) than in the actual code just so check the latter.
+ int fieldSizeBits = spec.getCurve().getField().getFieldSize();
+ int fieldSizeBytes = (fieldSizeBits % 8) == 0 ? (fieldSizeBits / 8) : (fieldSizeBits / 8) + 1;
+
+ byte[] encoded = ECSupport.encodeECPointUncompressed(spec.getGenerator(), spec.getCurve());
+ Assert.assertNotNull(encoded);
+ Assert.assertEquals(encoded.length, (fieldSizeBytes * 2) + 1);
+ Assert.assertEquals(encoded[0], 0x04);
+ Assert.assertEquals(new BigInteger(1, encoded, 1, fieldSizeBytes),
+ spec.getGenerator().getAffineX());
+ Assert.assertEquals(new BigInteger(1, encoded, fieldSizeBytes+1, fieldSizeBytes),
+ spec.getGenerator().getAffineY());
+
+ ECPoint decoded = ECSupport.decodeECPoint(encoded, spec.getCurve());
+ Assert.assertNotNull(decoded);
+ Assert.assertEquals(decoded, spec.getGenerator());
+ }
}
diff --git a/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/EnhancedECParameterSpecTest.java b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/EnhancedECParameterSpecTest.java
new file mode 100644
index 000000000..8cc8a9205
--- /dev/null
+++ b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/EnhancedECParameterSpecTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.security.crypto.ec;
+
+import java.security.KeyPairGenerator;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.opensaml.security.crypto.JCAConstants;
+import org.opensaml.security.crypto.ec.ECSupport;
+import org.opensaml.security.crypto.ec.EnhancedECParameterSpec;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ *
+ */
+public class EnhancedECParameterSpecTest extends BaseNamedCurveTest {
+
+ @Test(dataProvider = "namedCurves")
+ public void hashCodeAndEquals(String name) throws Exception {
+ // Use BC curve table + conversion as the control, so that we know the one we're getting isn't
+ // the same object that whatever provider is in effect produces when generating a KeyPair, etc.
+ // SunEC seems to produce the same object instance for all key pairs of a given curve, presumably
+ // they have an internal table of name -> ECParameterSpec.
+ ECParameterSpec controlInput = ECSupport.convert(ECNamedCurveTable.getParameterSpec(name));
+ Assert.assertNotNull(controlInput);
+
+ EnhancedECParameterSpec control = new EnhancedECParameterSpec(controlInput);
+
+ // As above, SunEC seems to generally always return the same ECParamterSpec for a given curve.
+ // However, this may not *always* be true, or another provider may be in use.
+ // So here just brute force and generate a bunch of key pairs and eval them against the control.
+ for (int i=0; i<25; i++) {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(JCAConstants.KEY_ALGO_EC);
+ kpg.initialize(new ECGenParameterSpec(name));
+ EnhancedECParameterSpec target = new EnhancedECParameterSpec(
+ ECPublicKey.class.cast(kpg.generateKeyPair().getPublic()).getParams());
+ Assert.assertNotSame(target.getOriginal(), control.getOriginal());
+ // This can't be guaranteed really, although it's almost always probably true.
+ //Assert.assertNotEquals(target.getOriginal().hashCode(), control.getOriginal().hashCode());
+ // This should always be true because the default impl just does reference equality, per default in Object.
+ Assert.assertNotEquals(target.getOriginal(), control.getOriginal());
+
+ Assert.assertNotSame(target, control);
+ Assert.assertEquals(target, control);
+ Assert.assertEquals(target.hashCode(), target.hashCode());
+ }
+
+ }
+
+}
diff --git a/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/NamedCurveRegistryTest.java b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/NamedCurveRegistryTest.java
new file mode 100644
index 000000000..6ac04dead
--- /dev/null
+++ b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/NamedCurveRegistryTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.security.crypto.ec;
+
+import java.security.spec.ECParameterSpec;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.opensaml.security.crypto.ec.curves.Secp256r1;
+import org.opensaml.security.crypto.ec.curves.Secp384r1;
+import org.opensaml.security.crypto.ec.curves.Secp521r1;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ *
+ */
+public class NamedCurveRegistryTest extends BaseNamedCurveTest {
+
+ @Test
+ public void basicOps() throws Exception {
+ Secp256r1 secp256r1 = new Secp256r1();
+ secp256r1.initialize();
+ Secp384r1 secp384r1 = new Secp384r1();
+ secp384r1.initialize();
+ Secp521r1 secp521r1 = new Secp521r1();
+ secp521r1.initialize();
+
+ // To test lookup by a different param spec object instance
+ ECParameterSpec secp256r1ParamSpec = ECSupport.convert(ECNamedCurveTable.getParameterSpec(secp256r1.getObjectIdentifier()));
+ ECParameterSpec secp384r1ParamSpec = ECSupport.convert(ECNamedCurveTable.getParameterSpec(secp384r1.getObjectIdentifier()));
+ ECParameterSpec secp521r1ParamSpec = ECSupport.convert(ECNamedCurveTable.getParameterSpec(secp521r1.getObjectIdentifier()));
+ Assert.assertNotNull(secp256r1ParamSpec);
+ Assert.assertNotNull(secp384r1ParamSpec);
+ Assert.assertNotNull(secp521r1ParamSpec);
+
+
+ NamedCurveRegistry registry = new NamedCurveRegistry();
+
+ Assert.assertTrue(registry.getRegisteredCurves().isEmpty());
+
+ Assert.assertNull(registry.getByName(secp256r1.getName()));
+ Assert.assertNull(registry.getByOID(secp256r1.getObjectIdentifier()));
+ Assert.assertNull(registry.getByURI(secp256r1.getURI()));
+ Assert.assertNull(registry.getByParameterSpec(secp256r1.getParameterSpec()));
+ Assert.assertNull(registry.getByParameterSpec(secp256r1ParamSpec));
+
+ registry.register(secp256r1);
+
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 1);
+ Assert.assertEquals(registry.getRegisteredCurves().stream().map(NamedCurve::getName).collect(Collectors.toSet()), Set.of("secp256r1"));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+
+ Assert.assertSame(registry.getByName(secp256r1.getName()), secp256r1);
+ Assert.assertSame(registry.getByOID(secp256r1.getObjectIdentifier()), secp256r1);
+ Assert.assertSame(registry.getByURI(secp256r1.getURI()), secp256r1);
+ Assert.assertSame(registry.getByParameterSpec(secp256r1.getParameterSpec()), secp256r1);
+ Assert.assertSame(registry.getByParameterSpec(secp256r1ParamSpec), secp256r1);
+
+ // Re-registering a different instance shouldn't cause a duplicate, etc, even though the object instance will change
+ secp256r1 = new Secp256r1();
+ secp256r1.initialize();
+ registry.register(secp256r1);
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 1);
+ Assert.assertEquals(registry.getRegisteredCurves().stream().map(NamedCurve::getName).collect(Collectors.toSet()), Set.of("secp256r1"));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+
+ registry.register(secp384r1);
+ registry.register(secp521r1);
+
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 3);
+ Assert.assertEquals(registry.getRegisteredCurves().stream().map(NamedCurve::getName).collect(Collectors.toSet()),
+ Set.of("secp256r1", "secp384r1", "secp521r1"));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp384r1));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp521r1));
+
+ Assert.assertSame(registry.getByName(secp384r1.getName()), secp384r1);
+ Assert.assertSame(registry.getByOID(secp384r1.getObjectIdentifier()), secp384r1);
+ Assert.assertSame(registry.getByURI(secp384r1.getURI()), secp384r1);
+ Assert.assertSame(registry.getByParameterSpec(secp384r1.getParameterSpec()), secp384r1);
+ Assert.assertSame(registry.getByParameterSpec(secp384r1ParamSpec), secp384r1);
+
+ Assert.assertSame(registry.getByName(secp521r1.getName()), secp521r1);
+ Assert.assertSame(registry.getByOID(secp521r1.getObjectIdentifier()), secp521r1);
+ Assert.assertSame(registry.getByURI(secp521r1.getURI()), secp521r1);
+ Assert.assertSame(registry.getByParameterSpec(secp521r1.getParameterSpec()), secp521r1);
+ Assert.assertSame(registry.getByParameterSpec(secp521r1ParamSpec), secp521r1);
+
+ registry.deregister(secp521r1);
+
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 2);
+ Assert.assertEquals(registry.getRegisteredCurves().stream().map(NamedCurve::getName).collect(Collectors.toSet()),
+ Set.of("secp256r1", "secp384r1"));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp384r1));
+ Assert.assertFalse(registry.getRegisteredCurves().contains(secp521r1));
+
+ Assert.assertNull(registry.getByName(secp521r1.getName()));
+ Assert.assertNull(registry.getByOID(secp521r1.getObjectIdentifier()));
+ Assert.assertNull(registry.getByURI(secp521r1.getURI()));
+ Assert.assertNull(registry.getByParameterSpec(secp521r1.getParameterSpec()));
+ Assert.assertNull(registry.getByParameterSpec(secp521r1ParamSpec));
+
+ registry.deregisterByName(secp256r1.getName());
+ registry.deregisterByOID(secp384r1.getObjectIdentifier());
+
+ Assert.assertTrue(registry.getRegisteredCurves().isEmpty());
+
+ registry.register(secp256r1);
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 1);
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+ registry.deregisterByURI(secp256r1.getURI());
+ Assert.assertTrue(registry.getRegisteredCurves().isEmpty());
+ Assert.assertFalse(registry.getRegisteredCurves().contains(secp256r1));
+
+ registry.register(secp256r1);
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 1);
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+ registry.deregisterByParameterSpec(secp256r1ParamSpec);
+ Assert.assertTrue(registry.getRegisteredCurves().isEmpty());
+ Assert.assertFalse(registry.getRegisteredCurves().contains(secp256r1));
+
+ registry.register(secp256r1);
+ registry.register(secp384r1);
+ Assert.assertEquals(registry.getRegisteredCurves().size(), 2);
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp256r1));
+ Assert.assertTrue(registry.getRegisteredCurves().contains(secp384r1));
+
+ registry.clear();
+ Assert.assertTrue(registry.getRegisteredCurves().isEmpty());
+ }
+
+ @Test
+ public void globalRegistry() {
+ NamedCurveRegistry registry = ECSupport.getGlobalNamedCurveRegistry();
+ Assert.assertNotNull(registry);
+
+ // Test that it has at least the 3 main ones.
+ Assert.assertTrue(registry.getRegisteredCurves().size() >= 3);
+ Set<String> curveNames = registry.getRegisteredCurves().stream().map(NamedCurve::getName).collect(Collectors.toSet());
+ Assert.assertTrue(curveNames.contains("secp256r1"));
+ Assert.assertTrue(curveNames.contains("secp384r1"));
+ Assert.assertTrue(curveNames.contains("secp521r1"));
+ }
+
+}
diff --git a/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/curves/NamedCurvesTest.java b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/curves/NamedCurvesTest.java
new file mode 100644
index 000000000..61d5fe4c6
--- /dev/null
+++ b/opensaml-security-api/src/test/java/org/opensaml/security/crypto/ec/curves/NamedCurvesTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.security.crypto.ec.curves;
+
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.opensaml.security.crypto.JCAConstants;
+import org.opensaml.security.crypto.KeySupport;
+import org.opensaml.security.crypto.ec.BaseNamedCurveTest;
+import org.opensaml.security.crypto.ec.ECSupport;
+import org.opensaml.security.crypto.ec.EnhancedECParameterSpec;
+import org.opensaml.security.crypto.ec.NamedCurve;
+import org.opensaml.security.crypto.ec.NamedCurveRegistry;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ *
+ */
+public class NamedCurvesTest extends BaseNamedCurveTest {
+
+ @Test(dataProvider = "namedCurves")
+ public void globalRegistryCurves(String namedCurve) throws Exception {
+ NamedCurveRegistry registry = ECSupport.getGlobalNamedCurveRegistry();
+ Assert.assertNotNull(registry);
+
+ ECParameterSpec bcSpec = ECSupport.convert(ECNamedCurveTable.getParameterSpec(namedCurve));
+ Assert.assertNotNull(bcSpec);
+
+ Assert.assertNotNull(registry.getByName(namedCurve));
+ Assert.assertEquals(registry.getByName(namedCurve).getName(), namedCurve);
+
+ Assert.assertNotNull(registry.getByParameterSpec(bcSpec));
+ // Use Enhanced- as a simple way to test equality via #equals(...).
+ // Wrap both. It seems TestNG does expected.equals(actual), but that may not always be true.
+ Assert.assertEquals(new EnhancedECParameterSpec(registry.getByParameterSpec(bcSpec).getParameterSpec()),
+ new EnhancedECParameterSpec(bcSpec));
+
+ NamedCurve curve = registry.getByName(namedCurve);
+ if (AbstractNamedCurve.class.isInstance(curve)) {
+ // Test the equality of the curve's spec #buildParameterSpec() against both BC and brute force from key pair generation
+ ECParameterSpec curveSpec = AbstractNamedCurve.class.cast(curve).buildParameterSpec();
+ Assert.assertNotNull(curveSpec);
+
+ ECParameterSpec jcaSpec = ECPublicKey.class.cast(
+ KeySupport.generateKeyPair(JCAConstants.KEY_ALGO_EC, new ECGenParameterSpec(namedCurve), null)
+ .getPublic()).getParams();
+ Assert.assertNotNull(jcaSpec);
+
+ // Wrap both sides, per above.
+ Assert.assertEquals(new EnhancedECParameterSpec(curveSpec), new EnhancedECParameterSpec(jcaSpec));
+ Assert.assertEquals(new EnhancedECParameterSpec(curveSpec), new EnhancedECParameterSpec(bcSpec));
+ }
+
+ }
+
+}
diff --git a/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/keyinfo/KeyInfoSupport.java b/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/keyinfo/KeyInfoSupport.java
index fdc929a1b..cd39cd5da 100644
--- a/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/keyinfo/KeyInfoSupport.java
+++ b/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/keyinfo/KeyInfoSupport.java
@@ -31,9 +31,13 @@ import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAParameterSpec;
import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.RSAPublicKeySpec;
@@ -55,16 +59,20 @@ import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.Credential;
+import org.opensaml.security.crypto.JCAConstants;
+import org.opensaml.security.crypto.ec.ECSupport;
import org.opensaml.security.x509.X509Support;
import org.opensaml.xmlsec.algorithm.AlgorithmSupport;
import org.opensaml.xmlsec.signature.DEREncodedKeyValue;
import org.opensaml.xmlsec.signature.DSAKeyValue;
+import org.opensaml.xmlsec.signature.ECKeyValue;
import org.opensaml.xmlsec.signature.Exponent;
import org.opensaml.xmlsec.signature.G;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.KeyName;
import org.opensaml.xmlsec.signature.KeyValue;
import org.opensaml.xmlsec.signature.Modulus;
+import org.opensaml.xmlsec.signature.NamedCurve;
import org.opensaml.xmlsec.signature.P;
import org.opensaml.xmlsec.signature.Q;
import org.opensaml.xmlsec.signature.RSAKeyValue;
@@ -525,10 +533,10 @@ public class KeyInfoSupport {
XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilderOrThrow(KeyValue.DEFAULT_ELEMENT_NAME);
final KeyValue keyValue = keyValueBuilder.buildObject(KeyValue.DEFAULT_ELEMENT_NAME);
- // TODO handle ECKeyValue
-
if (pk instanceof RSAPublicKey) {
keyValue.setRSAKeyValue(buildRSAKeyValue((RSAPublicKey) pk));
+ } else if (pk instanceof ECPublicKey) {
+ keyValue.setECKeyValue(buildECKeyValue((ECPublicKey) pk));
} else if (pk instanceof DSAPublicKey) {
keyValue.setDSAKeyValue(buildDSAKeyValue((DSAPublicKey) pk));
} else {
@@ -538,6 +546,53 @@ public class KeyInfoSupport {
keyInfo.getKeyValues().add(keyValue);
}
+ /**
+ * Builds an {@link ECKeyValue} XMLObject from the Java security EC public key type.
+ *
+ * <p>
+ * Only curve parameters specified by a {@link NamedCurve} are supported. Use of explicit
+ * curve parameters will throw.
+ * </p>
+ *
+ * @param ecPubKey a naive java {@link ECPublicKey}
+ * @return an {@link ECKeyValue} XMLObject
+ * @throws EncodingException if the NamedCurve variant was not used, if the EC PublicKey value is invalid
+ * or if the EC PublicKey value can not be Base64 encoded
+ */
+ @Nonnull public static ECKeyValue buildECKeyValue(@Nonnull final ECPublicKey ecPubKey) throws EncodingException {
+ Constraint.isNotNull(ecPubKey, "EC public key cannot be null");
+
+ final XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();
+
+ final ECKeyValue ecKeyValue = (ECKeyValue) builderFactory.getBuilderOrThrow(ECKeyValue.DEFAULT_ELEMENT_NAME)
+ .buildObject(ECKeyValue.DEFAULT_ELEMENT_NAME);
+
+ final NamedCurve namedCurve = (NamedCurve) builderFactory.getBuilderOrThrow(NamedCurve.DEFAULT_ELEMENT_NAME)
+ .buildObject(NamedCurve.DEFAULT_ELEMENT_NAME);
+
+ final org.opensaml.xmlsec.signature.PublicKey publicKey =
+ (org.opensaml.xmlsec.signature.PublicKey) builderFactory.getBuilderOrThrow(
+ org.opensaml.xmlsec.signature.PublicKey.DEFAULT_ELEMENT_NAME)
+ .buildObject(org.opensaml.xmlsec.signature.PublicKey.DEFAULT_ELEMENT_NAME);
+
+ final String uri = ECSupport.getNamedCurveURI(ecPubKey);
+ if (uri == null) {
+ // TODO EncdoingException doesn't really seem correct here, but we likely can't add a
+ // new checked exception type in a minor release. I guess it's sort of "encoding" though ...
+ throw new EncodingException("Unable to obtain NamedCurve URI from ECPublicKey");
+ }
+
+ namedCurve.setURI(uri);
+ ecKeyValue.setNamedCurve(namedCurve);
+
+ publicKey.setValue(Base64Support.encode(
+ ECSupport.encodeECPointUncompressed(ecPubKey.getW(), ecPubKey.getParams().getCurve()),
+ Base64Support.UNCHUNKED));
+ ecKeyValue.setPublicKey(publicKey);
+
+ return ecKeyValue;
+ }
+
/**
* Builds an {@link RSAKeyValue} XMLObject from the Java security RSA public key type.
*
@@ -656,8 +711,6 @@ public class KeyInfoSupport {
*/
@Nonnull public static List<PublicKey> getPublicKeys(@Nullable final KeyInfo keyInfo) throws KeyException {
- // TODO support ECKeyValue and DEREncodedKeyValue
-
final List<PublicKey> keys = new LinkedList<>();
if (keyInfo == null) {
@@ -696,11 +749,50 @@ public class KeyInfoSupport {
return getDSAKey(keyValue.getDSAKeyValue());
} else if (keyValue.getRSAKeyValue() != null) {
return getRSAKey(keyValue.getRSAKeyValue());
+ } else if (keyValue.getECKeyValue() != null) {
+ return getECKey(keyValue.getECKeyValue());
} else {
return null;
}
}
+ /**
+ * Builds an EC key from an {@link ECKeyValue} element.
+ *
+ * @param keyDescriptor the {@link ECKeyValue} key descriptor
+ *
+ * @return a new {@link ECPublicKey} instance of {@link PublicKey}
+ *
+ * @throws KeyException thrown if the key algorithm is not supported by the JCE or the key spec does not contain
+ * valid information
+ */
+ @Nonnull public static PublicKey getECKey(@Nonnull final ECKeyValue keyDescriptor) throws KeyException {
+
+ if (keyDescriptor.getNamedCurve() == null || keyDescriptor.getNamedCurve().getURI() == null) {
+ throw new KeyException("Only ECKeyValue NamedCurve representation is supported");
+ }
+
+ final ECParameterSpec ecParams =
+ ECSupport.getParameterSpecForURI(keyDescriptor.getNamedCurve().getURI());
+ if (ecParams == null) {
+ throw new KeyException("Could not resolve ECParametersSpec for NamedCurve URI: "
+ + keyDescriptor.getNamedCurve().getURI());
+ }
+
+ try {
+ final ECPoint ecPoint = ECSupport.decodeECPoint(
+ Base64Support.decode(keyDescriptor.getPublicKey().getValue()),
+ ecParams.getCurve());
+
+ final ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParams);
+
+ return buildKey(keySpec, JCAConstants.KEY_ALGO_EC);
+ } catch (final DecodingException e) {
+ throw new KeyException("Error Base64 decoding ECKeyValue PublicKey ECPoint", e);
+ }
+
+ }
+
/**
* Builds an DSA key from a {@link DSAKeyValue} element. The element must contain values for all required DSA public
* key parameters, including values for shared key family values P, Q and G.
@@ -746,7 +838,7 @@ public class KeyInfoSupport {
final DSAPublicKeySpec keySpec =
new DSAPublicKeySpec(yComponent, dsaParams.getP(), dsaParams.getQ(), dsaParams.getG());
- return buildKey(keySpec, "DSA");
+ return buildKey(keySpec, JCAConstants.KEY_ALGO_DSA);
}
/**
@@ -783,7 +875,7 @@ public class KeyInfoSupport {
final BigInteger exponent = keyDescriptor.getExponent().getValueBigInt();
final RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, exponent);
- return buildKey(keySpec, "RSA");
+ return buildKey(keySpec, JCAConstants.KEY_ALGO_RSA);
}
/**
@@ -851,7 +943,10 @@ public class KeyInfoSupport {
* @throws KeyException thrown if the given key data can not be converted into {@link PublicKey}
*/
@Nonnull public static PublicKey getKey(@Nonnull final DEREncodedKeyValue keyValue) throws KeyException{
- final String[] supportedKeyTypes = { "RSA", "DSA", "EC"};
+ final String[] supportedKeyTypes = {
+ JCAConstants.KEY_ALGO_RSA,
+ JCAConstants.KEY_ALGO_DSA,
+ JCAConstants.KEY_ALGO_EC};
Constraint.isNotNull(keyValue, "DEREncodedKeyValue cannot be null");
if (keyValue.getValue() == null) {
diff --git a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorFactory.java b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorFactory.java
index a33eba67c..b02f26804 100644
--- a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorFactory.java
+++ b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorFactory.java
@@ -28,6 +28,7 @@ import org.opensaml.security.credential.Credential;
import org.opensaml.xmlsec.agreement.KeyAgreementCredential;
import org.opensaml.xmlsec.agreement.KeyAgreementParameter;
import org.opensaml.xmlsec.agreement.XMLExpressableKeyAgreementParameter;
+import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.encryption.AgreementMethod;
import org.opensaml.xmlsec.encryption.KANonce;
import org.opensaml.xmlsec.encryption.OriginatorKeyInfo;
@@ -350,21 +351,11 @@ public class KeyAgreementKeyInfoGeneratorFactory extends BasicKeyInfoGeneratorFa
emitOriginatorKeyInfo = true;
emitRecipientKeyInfo = true;
- // TODO We can't default the general ones below until KeyInfoSupport supports PublicKey -> ECKeyValue
- // For now limit defaults to emit DEREncodedKeyValue only
- final KeyInfoGeneratorManager managerDEROnly = new KeyInfoGeneratorManager();
- final BasicKeyInfoGeneratorFactory basicFactoryDEROnly = new BasicKeyInfoGeneratorFactory();
- basicFactoryDEROnly.setEmitPublicDEREncodedKeyValue(true);
- managerDEROnly.registerFactory(basicFactoryDEROnly);
- originatorKeyInfoGeneratorManager = managerDEROnly;
- recipientKeyInfoGeneratorManager = managerDEROnly;
- /*
originatorKeyInfoGeneratorManager =
DefaultSecurityConfigurationBootstrap.buildBasicKeyInfoGeneratorManager().getDefaultManager();
recipientKeyInfoGeneratorManager =
DefaultSecurityConfigurationBootstrap.buildBasicKeyInfoGeneratorManager().getDefaultManager();
- */
}
/** {@inheritDoc} */
diff --git a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/provider/ECKeyValueProvider.java b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/provider/ECKeyValueProvider.java
new file mode 100644
index 000000000..c20e92027
--- /dev/null
+++ b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/keyinfo/impl/provider/ECKeyValueProvider.java
@@ -0,0 +1,115 @@
+/*
+ * 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.keyinfo.impl.provider;
+
+import java.security.KeyException;
+import java.security.PublicKey;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialContext;
+import org.opensaml.security.criteria.KeyAlgorithmCriterion;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
+import org.opensaml.xmlsec.keyinfo.impl.KeyInfoResolutionContext;
+import org.opensaml.xmlsec.signature.ECKeyValue;
+import org.opensaml.xmlsec.signature.KeyValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * Implementation of {@link org.opensaml.xmlsec.keyinfo.impl.KeyInfoProvider} which supports {@link ECKeyValue}.
+ */
+public class ECKeyValueProvider extends AbstractKeyInfoProvider {
+
+ /** Class logger. */
+ private final Logger log = LoggerFactory.getLogger(ECKeyValueProvider.class);
+
+ /** {@inheritDoc} */
+ public boolean handles(@Nonnull final XMLObject keyInfoChild) {
+ return getECKeyValue(keyInfoChild) != null;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable public Collection<Credential> process(@Nonnull final KeyInfoCredentialResolver resolver,
+ @Nonnull final XMLObject keyInfoChild, @Nullable final CriteriaSet criteriaSet,
+ @Nonnull final KeyInfoResolutionContext kiContext) throws SecurityException {
+
+ final ECKeyValue keyValue = getECKeyValue(keyInfoChild);
+ if (keyValue == null) {
+ return null;
+ }
+
+ if (criteriaSet != null) {
+ final KeyAlgorithmCriterion algorithmCriteria = criteriaSet.get(KeyAlgorithmCriterion.class);
+ if (algorithmCriteria != null && algorithmCriteria.getKeyAlgorithm() != null
+ && !"EC".equals(algorithmCriteria.getKeyAlgorithm())) {
+ log.debug("Criterion specified non-EC key algorithm, skipping");
+ return null;
+ }
+ }
+
+ log.debug("Attempting to extract credential from an RSAKeyValue");
+
+ PublicKey pubKey = null;
+ try {
+ pubKey = KeyInfoSupport.getECKey(keyValue);
+ } catch (final KeyException e) {
+ log.error("Error extracting EC key value: {}", e.getMessage());
+ throw new SecurityException("Error extracting EC key value", e);
+ }
+ final BasicCredential cred = new BasicCredential(pubKey);
+ cred.getKeyNames().addAll(kiContext.getKeyNames());
+
+ final CredentialContext credContext = buildCredentialContext(kiContext);
+ if (credContext != null) {
+ cred.getCredentialContextSet().add(credContext);
+ }
+
+ log.debug("Credential successfully extracted from ECKeyValue");
+ final LazySet<Credential> credentialSet = new LazySet<>();
+ credentialSet.add(cred);
+ return credentialSet;
+ }
+
+ /**
+ * Get the ECKeyValue from the passed XML object.
+ *
+ * @param xmlObject an XML object, presumably either a {@link KeyValue} or an {@link ECKeyValue}
+ * @return the ECKeyValue which was found, or null if none
+ */
+ @Nullable protected ECKeyValue getECKeyValue(@Nonnull final XMLObject xmlObject) {
+
+ if (xmlObject instanceof ECKeyValue) {
+ return (ECKeyValue) xmlObject;
+ } else if (xmlObject instanceof KeyValue) {
+ return ((KeyValue) xmlObject).getECKeyValue();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/ECKeyValueTest.java b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/ECKeyValueTest.java
new file mode 100644
index 000000000..da814945d
--- /dev/null
+++ b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/ECKeyValueTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.keyinfo.impl;
+
+import java.security.interfaces.ECPublicKey;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.opensaml.core.testing.XMLObjectBaseTestCase;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.crypto.KeySupport;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
+import org.opensaml.xmlsec.keyinfo.KeyInfoCriterion;
+import org.opensaml.xmlsec.keyinfo.impl.provider.ECKeyValueProvider;
+import org.opensaml.xmlsec.signature.KeyInfo;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+
+/**
+ * Test resolution of credentials from RSAKeyValue child of KeyInfo.
+ */
+public class ECKeyValueTest extends XMLObjectBaseTestCase {
+
+ private KeyInfoCredentialResolver resolver;
+
+ private String keyInfoFile;
+
+ private ECPublicKey pubKey;
+
+ /** Curve name: secp256r1, OID: 1.2.840.10045.3.1.7 */
+ private final String ecBase64 =
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBM0jGYrvVMpbVTT728+RfDLL0tPg" +
+ "swfUSUXfrXKwAGOmrSbF1KHsErZdXhnEC1VSmm9kTd8VzIi4OihEVMoU+w==";
+
+ @BeforeMethod
+ protected void setUp() throws Exception {
+ List<KeyInfoProvider> providers = new ArrayList<>();
+ providers.add(new ECKeyValueProvider());
+ resolver = new BasicProviderKeyInfoCredentialResolver(providers);
+ keyInfoFile = "/org/opensaml/xmlsec/keyinfo/impl/ECKeyValue.xml";
+ pubKey = KeySupport.buildJavaECPublicKey(ecBase64);
+ }
+
+ /**
+ * Test basic credential resolution.
+ *
+ * @throws ResolverException on error resolving credentials
+ */
+ @Test
+ public void testCredResolution() throws ResolverException {
+ KeyInfo keyInfo = (KeyInfo) unmarshallElement(keyInfoFile);
+ CriteriaSet criteriaSet = new CriteriaSet( new KeyInfoCriterion(keyInfo) );
+ Iterator<Credential> iter = resolver.resolve(criteriaSet).iterator();
+
+ Assert.assertTrue(iter.hasNext(), "No credentials were found");
+
+ Credential credential = iter.next();
+ Assert.assertNotNull(credential, "Credential was null");
+ Assert.assertFalse(iter.hasNext(), "Too many credentials returned");
+ Assert.assertTrue(credential instanceof BasicCredential, "Credential is not of the expected type");
+
+
+ Assert.assertNotNull(credential.getPublicKey(), "Public key was null");
+ Assert.assertEquals(credential.getPublicKey(), pubKey, "Expected public key value not found");
+
+ Assert.assertEquals(credential.getKeyNames().size(), 2, "Wrong number of key names");
+ Assert.assertTrue(credential.getKeyNames().contains("Foo"), "Expected key name value not found");
+ Assert.assertTrue(credential.getKeyNames().contains("Bar"), "Expected key name value not found");
+ }
+
+
+}
diff --git a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorTest.java b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorTest.java
index fa93ed144..89a744fb5 100644
--- a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorTest.java
+++ b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/impl/KeyAgreementKeyInfoGeneratorTest.java
@@ -111,22 +111,20 @@ public class KeyAgreementKeyInfoGeneratorTest extends XMLObjectBaseTestCase {
//Originator
Assert.assertNotNull(agreementMethod.getOriginatorKeyInfo());
OriginatorKeyInfo originatorKeyInfo = agreementMethod.getOriginatorKeyInfo();
- Assert.assertEquals(originatorKeyInfo.getOrderedChildren().size(), 1);
+ Assert.assertEquals(originatorKeyInfo.getOrderedChildren().size(), 2);
Assert.assertEquals(originatorKeyInfo.getDEREncodedKeyValues().size(), 1);
Assert.assertEquals(KeyInfoSupport.getKey(originatorKeyInfo.getDEREncodedKeyValues().get(0)), keyPairOriginatorECDH.getPublic());
- //TODO Can't do these until have support for ECKeyValue <-> PublicKey. Change KeyInfo children size above also 1 -> 2.
- //Assert.assertEquals(originatorKeyInfo.getKeyValues().size(), 1);
- //Assert.assertEquals(KeyInfoSupport.getKey(originatorKeyInfo.getKeyValues().get(0)), keyPairOriginatorECDH.getPublic());
+ Assert.assertEquals(originatorKeyInfo.getKeyValues().size(), 1);
+ Assert.assertEquals(KeyInfoSupport.getKey(originatorKeyInfo.getKeyValues().get(0)), keyPairOriginatorECDH.getPublic());
//Recipient
Assert.assertNotNull(agreementMethod.getRecipientKeyInfo());
RecipientKeyInfo recipientKeyInfo = agreementMethod.getRecipientKeyInfo();
- Assert.assertEquals(recipientKeyInfo.getOrderedChildren().size(), 1);
+ Assert.assertEquals(recipientKeyInfo.getOrderedChildren().size(), 2);
Assert.assertEquals(recipientKeyInfo.getDEREncodedKeyValues().size(), 1);
Assert.assertEquals(KeyInfoSupport.getKey(recipientKeyInfo.getDEREncodedKeyValues().get(0)), keyPairRecipientECDH.getPublic());
- //TODO Can't do these until have support for ECKeyValue <-> PublicKey. Change KeyInfo children size above also 1 -> 2.
- //Assert.assertEquals(recipientKeyInfo.getKeyValues().size(), 1);
- //Assert.assertEquals(KeyInfoSupport.getKey(recipientKeyInfo.getKeyValues().get(0)), keyPairRecipientECDH.getPublic());
+ Assert.assertEquals(recipientKeyInfo.getKeyValues().size(), 1);
+ Assert.assertEquals(KeyInfoSupport.getKey(recipientKeyInfo.getKeyValues().get(0)), keyPairRecipientECDH.getPublic());
//Params
Assert.assertEquals(agreementMethod.getUnknownXMLObjects(KeyDerivationMethod.DEFAULT_ELEMENT_NAME).size(), 1);
@@ -169,22 +167,20 @@ public class KeyAgreementKeyInfoGeneratorTest extends XMLObjectBaseTestCase {
//Originator
Assert.assertNotNull(agreementMethod.getOriginatorKeyInfo());
OriginatorKeyInfo originatorKeyInfo = agreementMethod.getOriginatorKeyInfo();
- Assert.assertEquals(originatorKeyInfo.getOrderedChildren().size(), 1);
+ Assert.assertEquals(originatorKeyInfo.getOrderedChildren().size(), 2);
Assert.assertEquals(originatorKeyInfo.getDEREncodedKeyValues().size(), 1);
Assert.assertEquals(KeyInfoSupport.getKey(originatorKeyInfo.getDEREncodedKeyValues().get(0)), keyPairOriginatorECDH.getPublic());
- //TODO Can't do these until have support for ECKeyValue <-> PublicKey. Change KeyInfo children size above also 1 -> 2.
- //Assert.assertEquals(originatorKeyInfo.getKeyValues().size(), 1);
- //Assert.assertEquals(KeyInfoSupport.getKey(originatorKeyInfo.getKeyValues().get(0)), keyPairOriginatorECDH.getPublic());
+ Assert.assertEquals(originatorKeyInfo.getKeyValues().size(), 1);
+ Assert.assertEquals(KeyInfoSupport.getKey(originatorKeyInfo.getKeyValues().get(0)), keyPairOriginatorECDH.getPublic());
//Recipient
Assert.assertNotNull(agreementMethod.getRecipientKeyInfo());
RecipientKeyInfo recipientKeyInfo = agreementMethod.getRecipientKeyInfo();
- Assert.assertEquals(recipientKeyInfo.getOrderedChildren().size(), 1);
+ Assert.assertEquals(recipientKeyInfo.getOrderedChildren().size(), 2);
Assert.assertEquals(recipientKeyInfo.getDEREncodedKeyValues().size(), 1);
Assert.assertEquals(KeyInfoSupport.getKey(recipientKeyInfo.getDEREncodedKeyValues().get(0)), keyPairRecipientECDH.getPublic());
- //TODO Can't do these until have support for ECKeyValue <-> PublicKey. Change KeyInfo children size above also 1 -> 2.
- //Assert.assertEquals(recipientKeyInfo.getKeyValues().size(), 1);
- //Assert.assertEquals(KeyInfoSupport.getKey(recipientKeyInfo.getKeyValues().get(0)), keyPairRecipientECDH.getPublic());
+ Assert.assertEquals(recipientKeyInfo.getKeyValues().size(), 1);
+ Assert.assertEquals(KeyInfoSupport.getKey(recipientKeyInfo.getKeyValues().get(0)), keyPairRecipientECDH.getPublic());
//Params
Assert.assertEquals(agreementMethod.getUnknownXMLObjects(KeyDerivationMethod.DEFAULT_ELEMENT_NAME).size(), 1);
diff --git a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/tests/KeyInfoSupportTest.java b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/tests/KeyInfoSupportTest.java
index 33121ae5b..ced4a8f5a 100644
--- a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/tests/KeyInfoSupportTest.java
+++ b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/keyinfo/tests/KeyInfoSupportTest.java
@@ -22,15 +22,20 @@ import org.testng.annotations.BeforeMethod;
import org.testng.Assert;
import java.math.BigInteger;
import java.security.KeyException;
+import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
import java.security.PublicKey;
+import java.security.Security;
import java.security.cert.CRLException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.List;
@@ -40,18 +45,23 @@ import net.shibboleth.utilities.java.support.codec.Base64Support;
import net.shibboleth.utilities.java.support.codec.DecodingException;
import net.shibboleth.utilities.java.support.codec.EncodingException;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.opensaml.core.testing.XMLObjectBaseTestCase;
import org.opensaml.security.SecurityException;
import org.opensaml.security.crypto.KeySupport;
+import org.opensaml.security.crypto.ec.ECSupport;
+import org.opensaml.security.crypto.ec.EnhancedECParameterSpec;
import org.opensaml.security.x509.X509Support;
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
import org.opensaml.xmlsec.signature.DEREncodedKeyValue;
import org.opensaml.xmlsec.signature.DSAKeyValue;
+import org.opensaml.xmlsec.signature.ECKeyValue;
import org.opensaml.xmlsec.signature.Exponent;
import org.opensaml.xmlsec.signature.G;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.KeyValue;
import org.opensaml.xmlsec.signature.Modulus;
+import org.opensaml.xmlsec.signature.NamedCurve;
import org.opensaml.xmlsec.signature.P;
import org.opensaml.xmlsec.signature.Q;
import org.opensaml.xmlsec.signature.RSAKeyValue;
@@ -178,7 +188,26 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
+ "/f9ymefKHB7ISlskT7kODCIbr5HHU/n1zXtMRjoslY1A+nFlWiAaIvjnj/C8x0BW"
+ "BkhuSKX/2PbljnmIdGV7mJK9/XUHnyKgZBxXEul2mlvGkrgUvyv+qYsCFsKSSrkB"
+ "1Mj2Ql5xmTMaePMEmvOr6fDAP0OH8cvADEZjx0s/5vvoBFPGGmPrHJluEVS0Fu8I" + "9sROg9YjyuhRV0b8xHo=";
-
+
+ /** Test EC key with named curve variant 1, curve: secp256r1, OID: 1.2.840.10045.3.1.7 */
+ private final String ecPubKey_NamedCurve1 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBM0jGYrvVMpbVTT728+RfDLL0tPg"
+ + "swfUSUXfrXKwAGOmrSbF1KHsErZdXhnEC1VSmm9kTd8VzIi4OihEVMoU+w==";
+
+ /** OID of the curve for ecPubKey_NamedCurve1, curve: secp256r1. */
+ private final String ecPubKey_NamedCurve1_OID = "1.2.840.10045.3.1.7";
+
+ /** Test EC key with explicit params variant 1, curve: secp256r1, OID: 1.2.840.10045.3.1.7 */
+ private final String ecPubKey_ExplicitParams1 = "MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA\n"
+ + "AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////"
+ + "///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd"
+ + "NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5"
+ + "RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA"
+ + "//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABBRJ1RlY9GqHxNRRPDh+rciw"
+ + "7HI/QgGWVf32j91hIwbQ8yNx0Hveirx0B5YGhF0cXrihKH0wC0zcYhtXUKHL7uQ=";
+
+ /** OID of the curve for ecPubKey_ExplicitParams1, curve: secp256r1. */
+ private final String ecPubKey_ExplicitParams1_OID = "1.2.840.10045.3.1.7";
+
private X509Certificate xmlCert1, xmlCert2;
private X509CRL xmlCRL1;
@@ -193,6 +222,8 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
private RSAKeyValue xmlRSAKeyValue1;
+ private ECKeyValue xmlECKeyValue_NamedCurve1, xmlECKeyValue_ExplicitParams1;
+
private int numExpectedCerts;
private int numExpectedCRLs;
@@ -204,6 +235,8 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
private RSAPublicKey javaRSAPubKey1;
private DSAPublicKey javaDSAPubKey1;
+
+ private ECPublicKey javaECPubKey_NamedCurve1, javaECPubKey_ExplicitParams1;
private DSAParams javaDSAParams1;
@@ -245,6 +278,9 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
javaDSAPubKey1 = KeySupport.buildJavaDSAPublicKey(dsaPubKey1);
javaRSAPubKey1 = KeySupport.buildJavaRSAPublicKey(rsaPubKey1);
+ javaECPubKey_NamedCurve1 = KeySupport.buildJavaECPublicKey(ecPubKey_NamedCurve1);
+ // SunEC provider doesn't support explicit params, so use a custom method.
+ javaECPubKey_ExplicitParams1 = buildECPublicKeyWithExplicitParams(ecPubKey_ExplicitParams1);
xmlRSAKeyValue1 = (RSAKeyValue) buildXMLObject(RSAKeyValue.DEFAULT_ELEMENT_NAME);
Modulus modulus = (Modulus) buildXMLObject(Modulus.DEFAULT_ELEMENT_NAME);
@@ -273,6 +309,31 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
y2.setValueBigInt(javaDSAPubKey1.getY());
xmlDSAKeyValue1NoParams.setY(y2);
javaDSAParams1 = javaDSAPubKey1.getParams();
+
+ xmlECKeyValue_NamedCurve1 = buildXMLObject(ECKeyValue.DEFAULT_ELEMENT_NAME);
+ NamedCurve namedCurve1 = buildXMLObject(NamedCurve.DEFAULT_ELEMENT_NAME);
+ org.opensaml.xmlsec.signature.PublicKey xmlECPublicKey_NamedCurve1 =
+ buildXMLObject(org.opensaml.xmlsec.signature.PublicKey.DEFAULT_ELEMENT_NAME);
+ namedCurve1.setURI("urn:oid:" + ecPubKey_NamedCurve1_OID);
+ xmlECPublicKey_NamedCurve1.setValue(Base64Support.encode(ECSupport.encodeECPointUncompressed(
+ javaECPubKey_NamedCurve1.getW(), javaECPubKey_NamedCurve1.getParams().getCurve()),
+ Base64Support.UNCHUNKED));
+ xmlECKeyValue_NamedCurve1.setNamedCurve(namedCurve1);
+ xmlECKeyValue_NamedCurve1.setPublicKey(xmlECPublicKey_NamedCurve1);
+
+ // Note: this is expressing the explicit params EC pub key as a named curve in KeyInfo.
+ // Update or add test cases + control data if we ever support the ECKeyValue with ECParameters variant.
+ xmlECKeyValue_ExplicitParams1 = buildXMLObject(ECKeyValue.DEFAULT_ELEMENT_NAME);
+ NamedCurve namedCurve_Explicit1 = buildXMLObject(NamedCurve.DEFAULT_ELEMENT_NAME);
+ org.opensaml.xmlsec.signature.PublicKey xmlECPublicKey_ExplicitParams1 =
+ buildXMLObject(org.opensaml.xmlsec.signature.PublicKey.DEFAULT_ELEMENT_NAME);
+ namedCurve_Explicit1.setURI("urn:oid:" + ecPubKey_ExplicitParams1_OID);
+ xmlECPublicKey_ExplicitParams1.setValue(Base64Support.encode(ECSupport.encodeECPointUncompressed(
+ javaECPubKey_ExplicitParams1.getW(), javaECPubKey_ExplicitParams1.getParams().getCurve()),
+ Base64Support.UNCHUNKED));
+ xmlECKeyValue_ExplicitParams1.setNamedCurve(namedCurve_Explicit1);
+ xmlECKeyValue_ExplicitParams1.setPublicKey(xmlECPublicKey_ExplicitParams1);
+
}
/**
@@ -445,12 +506,51 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
Assert.assertEquals(rsaKey, javaRSAPubKey1, "Generated key was not the expected value");
}
+ /** Test conversion of EC public keys from XML to Java security native type. */
+ @Test
+ public void testECConversionXMLToJavaWithNamedCurve() {
+ PublicKey key = null;
+ ECPublicKey ecKey = null;
+
+ try {
+ key = KeyInfoSupport.getECKey(xmlECKeyValue_NamedCurve1);
+ } catch (KeyException e) {
+ Assert.fail("RSA key conversion XML to Java failed: " + e);
+ }
+ ecKey = (ECPublicKey) key;
+ Assert.assertNotNull(ecKey, "Generated key was not an instance of ECPublicKey");
+ Assert.assertEquals(ecKey, javaECPubKey_NamedCurve1, "Generated key was not the expected value");
+ }
+
+ /** Test conversion of EC public keys from XML to Java security native type. */
+ @Test
+ public void testECConversionXMLToJavaWithExplicitParameters() {
+ PublicKey key = null;
+ ECPublicKey ecKey = null;
+
+ try {
+ key = KeyInfoSupport.getECKey(xmlECKeyValue_ExplicitParams1);
+ } catch (KeyException e) {
+ Assert.fail("RSA key conversion XML to Java failed: " + e);
+ }
+ ecKey = (ECPublicKey) key;
+ Assert.assertNotNull(ecKey, "Generated key was not an instance of ECPublicKey");
+ // The standard equals() test below doesn't work b/c the standard ECParameterSpec doesn't really implement equals()beyond
+ // the standard reference compare, and the control key is from BC, so the instance object isn't the same (constant one) as from SunEC.
+ // So use our Enhanced- equality wrapper helper instead.
+ //Assert.assertEquals(ecKey, javaECPubKey_ExplicitParams1);
+ Assert.assertEquals(ecKey.getW(), javaECPubKey_ExplicitParams1.getW());
+ Assert.assertEquals(new EnhancedECParameterSpec(ecKey.getParams()),
+ new EnhancedECParameterSpec(javaECPubKey_ExplicitParams1.getParams()));
+
+ }
+
/** Test conversion of DSA public keys from Java security native type to XML.
* @throws EncodingException on base64 encoding error*/
@Test
public void testDSAConversionJavaToXML() throws EncodingException {
DSAKeyValue dsaKeyValue = KeyInfoSupport.buildDSAKeyValue(javaDSAPubKey1);
- Assert.assertNotNull("Generated DSAKeyValue was null");
+ Assert.assertNotNull(dsaKeyValue);
Assert.assertEquals(dsaKeyValue
.getY().getValueBigInt(), javaDSAPubKey1.getY(), "Generated DSAKeyValue Y component was not the expected value");
Assert.assertEquals(dsaKeyValue.getP().getValueBigInt(), javaDSAPubKey1.getParams().getP(),
@@ -466,13 +566,41 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
@Test
public void testRSAConversionJavaToXML() throws EncodingException {
RSAKeyValue rsaKeyValue = KeyInfoSupport.buildRSAKeyValue(javaRSAPubKey1);
- Assert.assertNotNull("Generated RSAKeyValue was null");
+ Assert.assertNotNull(rsaKeyValue);
Assert.assertEquals(rsaKeyValue.getModulus().getValueBigInt(), javaRSAPubKey1.getModulus(),
"Generated RSAKeyValue modulus component was not the expected value");
Assert.assertEquals(rsaKeyValue.getExponent().getValueBigInt(),
javaRSAPubKey1.getPublicExponent(), "Generated RSAKeyValue exponent component was not the expected value");
}
+ /** Test conversion of EC public keys from Java security native type to XML.
+ * @throws EncodingException on base64 encoding error
+ * @throws DecodingException
+ * @throws KeyException */
+ @Test
+ public void testECConversionJavaToXMLWithNamedCurve() throws EncodingException, KeyException, DecodingException {
+ ECKeyValue ecKeyValue = KeyInfoSupport.buildECKeyValue(javaECPubKey_NamedCurve1);
+ Assert.assertNotNull(ecKeyValue);
+ Assert.assertEquals(ecKeyValue.getNamedCurve().getURI(), "urn:oid:" + ecPubKey_NamedCurve1_OID);
+ Assert.assertEquals(ECSupport.decodeECPoint(Base64Support.decode(ecKeyValue.getPublicKey().getValue()),
+ ECSupport.getNamedCurve("urn:oid:" + ecPubKey_NamedCurve1_OID).getParameterSpec().getCurve()),
+ javaECPubKey_NamedCurve1.getW());
+ }
+
+ /** Test conversion of EC public keys from Java security native type to XML.
+ * @throws EncodingException on base64 encoding error
+ * @throws DecodingException
+ * @throws KeyException */
+ @Test
+ public void testECConversionJavaToXMLWithExplicitParameters() throws EncodingException, KeyException, DecodingException {
+ ECKeyValue ecKeyValue = KeyInfoSupport.buildECKeyValue(javaECPubKey_ExplicitParams1);
+ Assert.assertNotNull(ecKeyValue);
+ Assert.assertEquals(ecKeyValue.getNamedCurve().getURI(), "urn:oid:" + ecPubKey_ExplicitParams1_OID);
+ Assert.assertEquals(ECSupport.decodeECPoint(Base64Support.decode(ecKeyValue.getPublicKey().getValue()),
+ ECSupport.getNamedCurve("urn:oid:" + ecPubKey_ExplicitParams1_OID).getParameterSpec().getCurve()),
+ javaECPubKey_ExplicitParams1.getW());
+ }
+
/** Tests extracting a DSA public key from a KeyValue. */
@Test
public void testGetDSAKey() {
@@ -561,6 +689,60 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
keyInfo.getKeyValues().clear();
}
+ /** Tests adding a public key as a KeyValue to KeyInfo.
+ * @throws EncodingException on base64 encoding error*/
+ @Test
+ public void testAddECPublicKeyWithNamedCurve() throws EncodingException {
+ keyInfo.getKeyValues().clear();
+
+ KeyInfoSupport.addPublicKey(keyInfo, javaECPubKey_NamedCurve1);
+ KeyValue kv = keyInfo.getKeyValues().get(0);
+ Assert.assertNotNull(kv, "KeyValue was null");
+ ECKeyValue ecKeyValue = kv.getECKeyValue();
+ Assert.assertNotNull(ecKeyValue, "ECKeyValue was null");
+
+ ECPublicKey javaKey = null;
+ try {
+ javaKey = (ECPublicKey) KeyInfoSupport.getECKey(ecKeyValue);
+ } catch (KeyException e) {
+ Assert.fail("Extraction of Java key failed: " + e);
+ }
+
+ Assert.assertEquals(javaKey, javaECPubKey_NamedCurve1, "Inserted EC public key with named curve was not the expected value");
+
+ keyInfo.getKeyValues().clear();
+ }
+
+ /** Tests adding a public key as a KeyValue to KeyInfo.
+ * @throws EncodingException on base64 encoding error*/
+ @Test
+ public void testAddECPublicKeyWithExplicitParams() throws EncodingException {
+ keyInfo.getKeyValues().clear();
+
+ KeyInfoSupport.addPublicKey(keyInfo, javaECPubKey_ExplicitParams1);
+ KeyValue kv = keyInfo.getKeyValues().get(0);
+ Assert.assertNotNull(kv, "KeyValue was null");
+ ECKeyValue ecKeyValue = kv.getECKeyValue();
+ Assert.assertNotNull(ecKeyValue, "ECKeyValue was null");
+
+ ECPublicKey javaKey = null;
+ try {
+ javaKey = (ECPublicKey) KeyInfoSupport.getECKey(ecKeyValue);
+ } catch (KeyException e) {
+ Assert.fail("Extraction of Java key failed: " + e);
+ }
+
+ // The standard equals() test below doesn't work b/c the standard ECParameterSpec doesn't really implement equals()beyond
+ // the standard reference compare, and the control key is from BC, so the instance object isn't the same (constant one) as from SunEC.
+ // So use our Enhanced- equality wrapper helper instead.
+ //Assert.assertEquals(javaKey, javaECPubKey_ExplicitParams1, "Inserted EC public key with explicit params was not the expected value");
+ Assert.assertEquals(javaKey.getW(), javaECPubKey_ExplicitParams1.getW());
+ Assert.assertEquals(new EnhancedECParameterSpec(javaKey.getParams()),
+ new EnhancedECParameterSpec(javaECPubKey_ExplicitParams1.getParams()));
+
+ keyInfo.getKeyValues().clear();
+ }
+
/** Tests adding a public key as a DEREncodedKeyValue to KeyInfo. */
@Test
public void testAddDEREncodedDSAPublicKey() {
@@ -730,4 +912,24 @@ public class KeyInfoSupportTest extends XMLObjectBaseTestCase {
Assert.assertNotNull(xmlValue, "Decoded X509Digest value was null");
Assert.assertTrue(Arrays.equals(digestValue, xmlValue), "Incorrect digest value");
}
+
+
+
+ //
+ // Helpers
+ //
+
+ private ECPublicKey buildECPublicKeyWithExplicitParams(String encodedKey) throws KeyException {
+ // Use BC for this for now, since standard Java's SunEC provider does not support explicit params
+ try {
+ Security.addProvider(new BouncyCastleProvider());
+ final KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
+ return (ECPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(Base64Support.decode(encodedKey)));
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException | DecodingException | NoSuchProviderException e) {
+ throw new KeyException("Failed creating ECPublicKey containing explict params", e);
+ } finally {
+ Security.removeProvider("BC");
+ }
+ }
+
}
\ No newline at end of file
diff --git a/opensaml-xmlsec-impl/src/test/resources/org/opensaml/xmlsec/keyinfo/impl/ECKeyValue.xml b/opensaml-xmlsec-impl/src/test/resources/org/opensaml/xmlsec/keyinfo/impl/ECKeyValue.xml
new file mode 100644
index 000000000..2372086d4
--- /dev/null
+++ b/opensaml-xmlsec-impl/src/test/resources/org/opensaml/xmlsec/keyinfo/impl/ECKeyValue.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:KeyName>Foo</ds:KeyName>
+ <ds:KeyName>Bar</ds:KeyName>
+ <ds:KeyValue>
+ <ds11:ECKeyValue xmlns:ds11="http://www.w3.org/2009/xmldsig11#">
+ <ds11:NamedCurve URI="urn:oid:1.2.840.10045.3.1.7"/>
+ <ds11:PublicKey>BATNIxmK71TKW1U0+9vPkXwyy9LT4LMH1ElF361ysABjpq0mxdSh7BK2XV4ZxAtVUppvZE3fFcyIuDooRFTKFPs=</ds11:PublicKey>
+ </ds11:ECKeyValue>
+ </ds:KeyValue>
+</ds:KeyInfo>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list