[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