feat: add message-oriented agreement contexts for DH, ECDH and XDH
Introduce GenericJcaMessageAgreementContext and KeyPairKey to support message-based key agreement without breaking existing AgreementContext capabilities. Key changes: - Add KeyPairKey wrapper to carry KeyPair through capability dispatch. - Introduce GenericJcaMessageAgreementContext implementing MessageAgreementContext, mapping the protocol message to an SPKI-encoded public key. - Extend DH, ECDH and XDH algorithms with an additional MessageAgreementContext capability while preserving existing PrivateKey-based agreement usage. - Improve core agreement tests to cover CLASSIC_AGREEMENT, PAIR_MESSAGE and KEM_ADAPTER variants with explicit branch identification. - Add demo samples illustrating practical usage patterns for ML-KEM and XDH agreement variants, including lifecycle and resource management guidance. This change adds capabilities by extension rather than replacement and keeps existing APIs and behaviors fully backward compatible. Signed-off-by: Leo Galambos <lg@hq.egothor.org>
This commit is contained in:
@@ -76,7 +76,7 @@ import zeroecho.core.context.AgreementContext;
|
|||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
*/
|
*/
|
||||||
public final class GenericJcaAgreementContext implements AgreementContext {
|
public class GenericJcaAgreementContext implements AgreementContext {
|
||||||
private final CryptoAlgorithm algorithm;
|
private final CryptoAlgorithm algorithm;
|
||||||
private final PrivateKey privateKey;
|
private final PrivateKey privateKey;
|
||||||
private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448")
|
private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448")
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement: This product includes software
|
||||||
|
* developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software without
|
||||||
|
* specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.core.alg.common.agreement;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import zeroecho.core.CryptoAlgorithm;
|
||||||
|
import zeroecho.core.context.MessageAgreementContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message-oriented JCA key agreement context where the handshake message is a
|
||||||
|
* public key encoding.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This context provides a {@link MessageAgreementContext} view over a classical
|
||||||
|
* JCA Diffie–Hellman style agreement (e.g., ECDH, XDH). The protocol
|
||||||
|
* "to-be-sent" data for such agreements is the party's public key. This class
|
||||||
|
* therefore maps:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #getPeerMessage()} to the local public key encoding (X.509
|
||||||
|
* SubjectPublicKeyInfo),</li>
|
||||||
|
* <li>{@link #setPeerMessage(byte[])} to importing the peer public key and
|
||||||
|
* binding it as the peer key.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Encoding</h2>
|
||||||
|
* <p>
|
||||||
|
* The message format is the standard X.509 SubjectPublicKeyInfo encoding
|
||||||
|
* returned by {@link java.security.PublicKey#getEncoded()}. This format is
|
||||||
|
* stable, widely interoperable and avoids ad-hoc or algorithm-specific wire
|
||||||
|
* formats.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Algorithm and provider selection</h2>
|
||||||
|
* <p>
|
||||||
|
* The underlying key agreement algorithm name (for
|
||||||
|
* {@link javax.crypto.KeyAgreement}) and the key factory algorithm name (for
|
||||||
|
* {@link KeyFactory}) are supplied explicitly by the algorithm registration
|
||||||
|
* code. This keeps algorithm-specific knowledge in {@code *Algorithm} classes
|
||||||
|
* rather than embedding naming heuristics into shared code.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Lifecycle</h2>
|
||||||
|
* <ol>
|
||||||
|
* <li>Create the context with the local key pair wrapper.</li>
|
||||||
|
* <li>Send {@link #getPeerMessage()} to the remote party.</li>
|
||||||
|
* <li>Receive the remote party's message and call
|
||||||
|
* {@link #setPeerMessage(byte[])}.</li>
|
||||||
|
* <li>Derive the raw shared secret with {@link #deriveSecret()}.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Security considerations</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>The message returned by {@link #getPeerMessage()} contains only public
|
||||||
|
* key material.</li>
|
||||||
|
* <li>The output of {@link #deriveSecret()} is a raw shared secret and must be
|
||||||
|
* processed with a KDF by higher protocol layers before use as symmetric keying
|
||||||
|
* material.</li>
|
||||||
|
* <li>This class does not log, persist, or otherwise expose private key
|
||||||
|
* material.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread safety</h2>
|
||||||
|
* <p>
|
||||||
|
* Instances are not thread-safe and are intended for single-use,
|
||||||
|
* single-threaded protocol executions.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext
|
||||||
|
implements MessageAgreementContext {
|
||||||
|
|
||||||
|
private final PublicKey localPublic;
|
||||||
|
private final String keyFactoryAlg;
|
||||||
|
private final String keyFactoryProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a message-oriented agreement context over a JCA key agreement.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The wrapped {@link KeyPairKey} supplies both local private and public key
|
||||||
|
* components: the private key is used for the underlying agreement computation,
|
||||||
|
* while the public key is exported as the handshake message via
|
||||||
|
* {@link #getPeerMessage()}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param alg algorithm descriptor that owns this context
|
||||||
|
* @param keyPairKey local key pair wrapper; must contain both a private
|
||||||
|
* and a public key
|
||||||
|
* @param jcaAgreementName JCA {@link javax.crypto.KeyAgreement} algorithm
|
||||||
|
* name (e.g., {@code "ECDH"}, {@code "X25519"})
|
||||||
|
* @param agreementProvider optional JCA provider name for
|
||||||
|
* {@link javax.crypto.KeyAgreement}, or {@code null}
|
||||||
|
* for default provider selection
|
||||||
|
* @param keyFactoryAlg JCA {@link KeyFactory} algorithm name used to
|
||||||
|
* import peer public keys (e.g., {@code "EC"},
|
||||||
|
* {@code "XDH"})
|
||||||
|
* @param keyFactoryProvider optional JCA provider name for {@link KeyFactory},
|
||||||
|
* or {@code null} for default provider selection
|
||||||
|
* @throws NullPointerException if any required argument is {@code null}
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName,
|
||||||
|
String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) {
|
||||||
|
super(Objects.requireNonNull(alg, "alg"), Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(),
|
||||||
|
Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider);
|
||||||
|
this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public");
|
||||||
|
this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg");
|
||||||
|
this.keyFactoryProvider = keyFactoryProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local party's handshake message.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For DH/XDH-style agreements, the handshake message is the local public key
|
||||||
|
* encoding. The returned array is a defensive copy and may be safely
|
||||||
|
* transmitted over untrusted channels.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The encoding is the standard X.509 SubjectPublicKeyInfo bytes returned by
|
||||||
|
* {@link PublicKey#getEncoded()}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a defensive copy of the local public key encoding (never
|
||||||
|
* {@code null})
|
||||||
|
* @throws IllegalStateException if the local public key does not provide an
|
||||||
|
* encoding
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] getPeerMessage() {
|
||||||
|
byte[] encoded = localPublic.getEncoded();
|
||||||
|
if (encoded == null) {
|
||||||
|
throw new IllegalStateException("Local public key does not provide an encoding");
|
||||||
|
}
|
||||||
|
return encoded.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supplies the peer party's handshake message.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The provided message is interpreted as an X.509 SubjectPublicKeyInfo encoding
|
||||||
|
* of the peer public key. The key is imported using {@link KeyFactory} and then
|
||||||
|
* assigned as the peer key for the underlying agreement computation.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Passing {@code null} resets the peer binding and makes
|
||||||
|
* {@link #deriveSecret()} unusable until a new peer message is provided.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param message peer public key encoding (SPKI), or {@code null} to reset the
|
||||||
|
* peer state
|
||||||
|
* @throws IllegalArgumentException if the message cannot be imported as a
|
||||||
|
* public key using the configured
|
||||||
|
* {@link KeyFactory} algorithm/provider
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setPeerMessage(byte[] message) {
|
||||||
|
if (message == null) {
|
||||||
|
setPeerPublic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublicKey peerPublic = importPeerPublic(message);
|
||||||
|
setPeerPublic(peerPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a peer public key from an X.509 SubjectPublicKeyInfo encoding.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method performs no caching and always imports the key anew. The caller
|
||||||
|
* is responsible for ensuring that the protocol-layer validation requirements
|
||||||
|
* are met (e.g., checking that the received public key belongs to an expected
|
||||||
|
* identity, or that the agreement mode requires ephemeral vs. static keys).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param spkiEncoded peer public key encoding (SPKI); must not be {@code null}
|
||||||
|
* @return imported peer {@link PublicKey}
|
||||||
|
* @throws IllegalArgumentException if the key cannot be imported
|
||||||
|
*/
|
||||||
|
private PublicKey importPeerPublic(byte[] spkiEncoded) {
|
||||||
|
try {
|
||||||
|
KeyFactory keyFactory = (keyFactoryProvider == null) ? KeyFactory.getInstance(keyFactoryAlg)
|
||||||
|
: KeyFactory.getInstance(keyFactoryAlg, keyFactoryProvider);
|
||||||
|
|
||||||
|
X509EncodedKeySpec spec = new X509EncodedKeySpec(spkiEncoded);
|
||||||
|
return keyFactory.generatePublic(spec);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.core.alg.common.agreement;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Key} wrapper that carries a {@link KeyPair} through APIs that are
|
||||||
|
* constrained to {@link Key} types.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This type exists to support capabilities that require both private and public
|
||||||
|
* components (e.g., message-oriented key agreement contexts), while preserving
|
||||||
|
* backward-compatible capabilities that accept only a {@link PrivateKey}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The wrapper does not expose an encoding via {@link #getEncoded()} because
|
||||||
|
* serializing private key material implicitly is dangerous and not required for
|
||||||
|
* capability dispatch.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class KeyPairKey implements Key, Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final KeyPair keyPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wrapper around a {@link KeyPair}.
|
||||||
|
*
|
||||||
|
* @param keyPair key pair to wrap (must not be {@code null})
|
||||||
|
* @throws NullPointerException if {@code keyPair} is {@code null}
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public KeyPairKey(KeyPair keyPair) {
|
||||||
|
this.keyPair = Objects.requireNonNull(keyPair, "keyPair");
|
||||||
|
Objects.requireNonNull(keyPair.getPrivate(), "keyPair.private");
|
||||||
|
Objects.requireNonNull(keyPair.getPublic(), "keyPair.public");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wrapped {@link KeyPair}.
|
||||||
|
*
|
||||||
|
* @return key pair
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public KeyPair keyPair() {
|
||||||
|
return keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wrapped private key.
|
||||||
|
*
|
||||||
|
* @return private key
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public PrivateKey privateKey() {
|
||||||
|
return keyPair.getPrivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wrapped public key.
|
||||||
|
*
|
||||||
|
* @return public key
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public PublicKey publicKey() {
|
||||||
|
return keyPair.getPublic();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the algorithm name of the wrapped private key.
|
||||||
|
*
|
||||||
|
* @return algorithm name
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return privateKey().getAlgorithm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code null}. This wrapper intentionally does not define a standard
|
||||||
|
* encoding format.
|
||||||
|
*
|
||||||
|
* @return {@code null}
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFormat() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code null}. This wrapper intentionally does not expose a combined
|
||||||
|
* encoding.
|
||||||
|
*
|
||||||
|
* @return {@code null}
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] getEncoded() {
|
||||||
|
return null; // NOPMD
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,10 @@ import zeroecho.core.AlgorithmFamily;
|
|||||||
import zeroecho.core.KeyUsage;
|
import zeroecho.core.KeyUsage;
|
||||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||||
|
import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||||
|
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||||
import zeroecho.core.context.AgreementContext;
|
import zeroecho.core.context.AgreementContext;
|
||||||
|
import zeroecho.core.context.MessageAgreementContext;
|
||||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,6 +136,10 @@ public final class DhAlgorithm extends AbstractCryptoAlgorithm {
|
|||||||
DhSpec.class,
|
DhSpec.class,
|
||||||
(PrivateKey k, DhSpec s) -> new GenericJcaAgreementContext(this, k, "DiffieHellman", null),
|
(PrivateKey k, DhSpec s) -> new GenericJcaAgreementContext(this, k, "DiffieHellman", null),
|
||||||
DhSpec::ffdhe2048);
|
DhSpec::ffdhe2048);
|
||||||
|
capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
|
||||||
|
DhSpec.class, (KeyPairKey k, DhSpec s) -> new GenericJcaMessageAgreementContext(this, k,
|
||||||
|
"DiffieHellman", null, "DH", null),
|
||||||
|
DhSpec::ffdhe2048);
|
||||||
|
|
||||||
registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
|
registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
|
||||||
registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ import zeroecho.core.AlgorithmFamily;
|
|||||||
import zeroecho.core.KeyUsage;
|
import zeroecho.core.KeyUsage;
|
||||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||||
|
import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||||
|
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||||
import zeroecho.core.alg.ecdsa.EcdsaCurveSpec;
|
import zeroecho.core.alg.ecdsa.EcdsaCurveSpec;
|
||||||
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
|
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
|
||||||
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
|
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
|
||||||
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
|
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
|
||||||
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
|
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
|
||||||
import zeroecho.core.context.AgreementContext;
|
import zeroecho.core.context.AgreementContext;
|
||||||
|
import zeroecho.core.context.MessageAgreementContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <h2>Elliptic Curve Diffie-Hellman (ECDH) Algorithm</h2>
|
* <h2>Elliptic Curve Diffie-Hellman (ECDH) Algorithm</h2>
|
||||||
@@ -152,6 +155,10 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
|
|||||||
EcdsaCurveSpec.class,
|
EcdsaCurveSpec.class,
|
||||||
(PrivateKey k, EcdsaCurveSpec s) -> new GenericJcaAgreementContext(this, k, "ECDH", null),
|
(PrivateKey k, EcdsaCurveSpec s) -> new GenericJcaAgreementContext(this, k, "ECDH", null),
|
||||||
() -> EcdsaCurveSpec.P256); // XXX spec is not used at all ?!?!
|
() -> EcdsaCurveSpec.P256); // XXX spec is not used at all ?!?!
|
||||||
|
capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
|
||||||
|
EcdsaCurveSpec.class, (KeyPairKey k, EcdsaCurveSpec s) -> new GenericJcaMessageAgreementContext(this, k,
|
||||||
|
"ECDH", null, "EC", null),
|
||||||
|
() -> EcdsaCurveSpec.P256);
|
||||||
|
|
||||||
// Reuse EC builders/importers
|
// Reuse EC builders/importers
|
||||||
registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
|
registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ import zeroecho.core.AlgorithmFamily;
|
|||||||
import zeroecho.core.KeyUsage;
|
import zeroecho.core.KeyUsage;
|
||||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||||
|
import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||||
|
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||||
import zeroecho.core.context.AgreementContext;
|
import zeroecho.core.context.AgreementContext;
|
||||||
|
import zeroecho.core.context.MessageAgreementContext;
|
||||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,6 +148,11 @@ public final class XdhAlgorithm extends AbstractCryptoAlgorithm {
|
|||||||
XdhSpec.class,
|
XdhSpec.class,
|
||||||
(PrivateKey k, XdhSpec s) -> new GenericJcaAgreementContext(this, k, s.keyAgreementName(), null),
|
(PrivateKey k, XdhSpec s) -> new GenericJcaAgreementContext(this, k, s.keyAgreementName(), null),
|
||||||
() -> XdhSpec.X25519);
|
() -> XdhSpec.X25519);
|
||||||
|
// New capability: MessageAgreementContext over KeyPair
|
||||||
|
capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
|
||||||
|
XdhSpec.class, (KeyPairKey k, XdhSpec s) -> new GenericJcaMessageAgreementContext(this, k,
|
||||||
|
s.keyAgreementName(), null, "XDH", null),
|
||||||
|
() -> XdhSpec.X25519);
|
||||||
|
|
||||||
registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519);
|
registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519);
|
||||||
registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ package zeroecho.core.alg.common.agreement;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
@@ -88,20 +87,26 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
System.out.println(method + "...ok");
|
System.out.println(method + "...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void printCapabilityHeader(String algId, String branch, Capability cap) {
|
||||||
|
System.out.println("...algId=" + algId + ", branch=" + branch + ", ctxType=" + cap.contextType().getSimpleName()
|
||||||
|
+ ", keyType=" + cap.keyType().getSimpleName() + ", specType=" + cap.specType().getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
private static String hex(byte[] b) {
|
private static String hex(byte[] b) {
|
||||||
if (b == null) {
|
if (b == null) {
|
||||||
return "null";
|
return "null";
|
||||||
}
|
}
|
||||||
StringBuilder sb = new StringBuilder(b.length * 2);
|
StringBuilder sb = new StringBuilder(b.length * 2);
|
||||||
for (byte v : b) {
|
for (int i = 0; i < b.length; i++) {
|
||||||
if (sb.length() == 80) {
|
if (sb.length() == 80) {
|
||||||
sb.append("...");
|
sb.append("...");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if ((v & 0xFF) < 16) {
|
int v = b[i] & 0xFF;
|
||||||
|
if (v < 16) {
|
||||||
sb.append('0');
|
sb.append('0');
|
||||||
}
|
}
|
||||||
sb.append(Integer.toHexString(v & 0xFF));
|
sb.append(Integer.toHexString(v));
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
@@ -118,8 +123,13 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
static void setup() {
|
static void setup() {
|
||||||
// If needed, install/activate providers (e.g., BouncyCastlePQCProvider) here.
|
// Optional: activate providers (e.g., BC/BCPQC) if present.
|
||||||
|
// Keep tests runnable even if BC is absent.
|
||||||
|
try {
|
||||||
BouncyCastleActivator.init();
|
BouncyCastleActivator.init();
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -135,43 +145,140 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final List<Capability> caps = alg.listCapabilities();
|
final List<Capability> caps = alg.listCapabilities();
|
||||||
|
|
||||||
for (Capability cap : caps) {
|
for (Capability cap : caps) {
|
||||||
if (cap.role() != KeyUsage.AGREEMENT) {
|
if (cap.role() != KeyUsage.AGREEMENT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.printf(" ...%s - AGREEMENT capability found", id);
|
System.out.println("...algId=" + id + " - AGREEMENT capability found");
|
||||||
|
|
||||||
final Class<?> ctxType = cap.contextType();
|
final Class<?> ctxType = cap.contextType();
|
||||||
final Class<?> keyType = cap.keyType();
|
final Class<?> keyType = cap.keyType();
|
||||||
final Class<?> specType = cap.specType();
|
final Class<?> specType = cap.specType();
|
||||||
@SuppressWarnings("unused")
|
|
||||||
final ContextSpec defVal = cap.defaultSpec().get();
|
|
||||||
|
|
||||||
// System.out.printf(" ......type=[%s] key=[%s] spec=[%s] default=[%s]%n",
|
// --------------------------------------------------------------------
|
||||||
// ctxType, keyType, specType, defVal);
|
// MessageAgreementContext branch A: PAIR_MESSAGE (DH/XDH/ECDH-style)
|
||||||
|
//
|
||||||
|
// New extension:
|
||||||
|
// - contextType: MessageAgreementContext
|
||||||
|
// - keyType: KeyPairKey (wrapper for KeyPair, because capabilities require Key)
|
||||||
|
// - specType: ContextSpec (algorithm-specific agreement spec)
|
||||||
|
//
|
||||||
|
// Semantics:
|
||||||
|
// - getPeerMessage() returns local public key encoding (SPKI)
|
||||||
|
// - setPeerMessage(...) imports peer public key encoding
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
if (MessageAgreementContext.class.isAssignableFrom(ctxType) && keyType == KeyPairKey.class
|
||||||
|
&& ContextSpec.class.isAssignableFrom(specType)) {
|
||||||
|
|
||||||
// AGREEMENT (KEM-style) via MessageAgreementContext adapter
|
printCapabilityHeader(alg.id(), "PAIR_MESSAGE", cap);
|
||||||
if (MessageAgreementContext.class.isAssignableFrom(ctxType)) {
|
|
||||||
|
|
||||||
KeyPair bob = generateKeyPair(alg);
|
ContextSpec spec = null;
|
||||||
if (bob == null) {
|
try {
|
||||||
System.out.println(" ...bob=null");
|
spec = cap.defaultSpec().get();
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
spec = tryExtractContextSpec(alg);
|
||||||
|
}
|
||||||
|
if (spec == null) {
|
||||||
|
System.out.println("...spec=null (skip)");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("runAgreement(" + alg.id() + ", VoidSpec)");
|
KeyPair alice = generateKeyPair(alg);
|
||||||
System.out.println(" Bob.public " + keyInfo("key", bob.getPublic()));
|
KeyPair bob = generateKeyPair(alg);
|
||||||
System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate()));
|
if (alice == null || bob == null) {
|
||||||
|
System.out.println("...keypair=null (skip)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("...pair-message agreement roundtrip: algId=" + alg.id() + ", spec="
|
||||||
|
+ spec.getClass().getSimpleName());
|
||||||
|
System.out.println("...Alice.public " + keyInfo("key", alice.getPublic()));
|
||||||
|
System.out.println("...Alice.private " + keyInfo("key", alice.getPrivate()));
|
||||||
|
System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
|
||||||
|
System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||||
|
|
||||||
|
KeyPairKey aliceKey = new KeyPairKey(alice);
|
||||||
|
KeyPairKey bobKey = new KeyPairKey(bob);
|
||||||
|
|
||||||
|
MessageAgreementContext aCtx = null;
|
||||||
|
MessageAgreementContext bCtx = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec);
|
||||||
|
bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bobKey, spec);
|
||||||
|
|
||||||
|
byte[] aMsg = aCtx.getPeerMessage();
|
||||||
|
byte[] bMsg = bCtx.getPeerMessage();
|
||||||
|
|
||||||
|
aCtx.setPeerMessage(bMsg);
|
||||||
|
bCtx.setPeerMessage(aMsg);
|
||||||
|
|
||||||
|
byte[] zA = aCtx.deriveSecret();
|
||||||
|
byte[] zB = bCtx.deriveSecret();
|
||||||
|
|
||||||
|
System.out.println("...Alice.msg =" + hex(aMsg));
|
||||||
|
System.out.println("...Bob.msg =" + hex(bMsg));
|
||||||
|
System.out.println("...Alice.secret =" + hex(zA));
|
||||||
|
System.out.println("...Bob.secret =" + hex(zB));
|
||||||
|
|
||||||
|
assertArrayEquals(zA, zB, alg.id() + ": PAIR_MESSAGE secrets mismatch");
|
||||||
|
System.out.println("...PAIR_MESSAGE...ok");
|
||||||
|
} finally {
|
||||||
|
if (aCtx != null) {
|
||||||
|
try {
|
||||||
|
aCtx.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bCtx != null) {
|
||||||
|
try {
|
||||||
|
bCtx.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a given algorithm, one AGREEMENT capability is sufficient for the sweep.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// MessageAgreementContext branch B: KEM_ADAPTER (KEM-style adapter)
|
||||||
|
//
|
||||||
|
// Existing behavior:
|
||||||
|
// - initiator has recipient PublicKey (encapsulation)
|
||||||
|
// - responder has PrivateKey (decapsulation)
|
||||||
|
// - spec is VoidSpec
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
if (MessageAgreementContext.class.isAssignableFrom(ctxType)
|
||||||
|
&& (keyType == PublicKey.class || keyType == PrivateKey.class)) {
|
||||||
|
|
||||||
|
printCapabilityHeader(alg.id(), "KEM_ADAPTER", cap);
|
||||||
|
|
||||||
|
KeyPair bob = generateKeyPair(alg);
|
||||||
|
if (bob == null) {
|
||||||
|
System.out.println("...keypair=null (skip)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("...kem-adapter agreement roundtrip: algId=" + alg.id() + ", spec=VoidSpec");
|
||||||
|
System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
|
||||||
|
System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||||
|
|
||||||
|
MessageAgreementContext aliceCtx = null;
|
||||||
|
MessageAgreementContext bobCtx = null;
|
||||||
|
|
||||||
|
try {
|
||||||
// Alice (initiator): has Bob's public key
|
// Alice (initiator): has Bob's public key
|
||||||
MessageAgreementContext aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT,
|
aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(),
|
||||||
bob.getPublic(), VoidSpec.INSTANCE);
|
VoidSpec.INSTANCE);
|
||||||
|
|
||||||
// Bob (responder): has his private key
|
// Bob (responder): has his private key
|
||||||
MessageAgreementContext bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT,
|
bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
|
||||||
bob.getPrivate(), VoidSpec.INSTANCE);
|
VoidSpec.INSTANCE);
|
||||||
|
|
||||||
// Initiator produces encapsulation message (ciphertext) to send
|
// Initiator produces encapsulation message (ciphertext) to send
|
||||||
byte[] enc = aliceCtx.getPeerMessage();
|
byte[] enc = aliceCtx.getPeerMessage();
|
||||||
@@ -182,86 +289,112 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
byte[] kA = aliceCtx.deriveSecret();
|
byte[] kA = aliceCtx.deriveSecret();
|
||||||
byte[] kB = bobCtx.deriveSecret();
|
byte[] kB = bobCtx.deriveSecret();
|
||||||
|
|
||||||
System.out.println(" KEM.ciphertext=" + hex(enc));
|
System.out.println("...KEM.ciphertext=" + hex(enc));
|
||||||
System.out.println(" Alice.secret =" + hex(kA));
|
System.out.println("...Alice.secret =" + hex(kA));
|
||||||
System.out.println(" Bob.secret =" + hex(kB));
|
System.out.println("...Bob.secret =" + hex(kB));
|
||||||
|
|
||||||
assertArrayEquals(kA, kB, alg.id() + ": agreement secrets mismatch");
|
|
||||||
System.out.println("...ok");
|
|
||||||
|
|
||||||
|
assertArrayEquals(kA, kB, alg.id() + ": KEM_ADAPTER secrets mismatch");
|
||||||
|
System.out.println("...KEM_ADAPTER...ok");
|
||||||
|
} finally {
|
||||||
|
if (aliceCtx != null) {
|
||||||
try {
|
try {
|
||||||
aliceCtx.close();
|
aliceCtx.close();
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (bobCtx != null) {
|
||||||
try {
|
try {
|
||||||
bobCtx.close();
|
bobCtx.close();
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Classic DH/XDH: AgreementContext + real ContextSpec --------
|
// --------------------------------------------------------------------
|
||||||
else if (AgreementContext.class.isAssignableFrom(ctxType)
|
// AgreementContext branch C: CLASSIC_AGREEMENT (PrivateKey + ContextSpec)
|
||||||
&& ContextSpec.class.isAssignableFrom(specType) && keyType == PrivateKey.class) {
|
// --------------------------------------------------------------------
|
||||||
|
if (AgreementContext.class.isAssignableFrom(ctxType) && keyType == PrivateKey.class
|
||||||
|
&& ContextSpec.class.isAssignableFrom(specType)) {
|
||||||
|
|
||||||
KeyPair alice;
|
printCapabilityHeader(alg.id(), "CLASSIC_AGREEMENT", cap);
|
||||||
KeyPair bob;
|
|
||||||
|
|
||||||
alice = generateKeyPair(alg);
|
|
||||||
bob = generateKeyPair(alg);
|
|
||||||
|
|
||||||
if (alice == null || bob == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer the capability's default ContextSpec if provided
|
|
||||||
ContextSpec spec = null;
|
ContextSpec spec = null;
|
||||||
try {
|
try {
|
||||||
ContextSpec def = cap.defaultSpec().get();
|
spec = cap.defaultSpec().get();
|
||||||
spec = def;
|
|
||||||
} catch (Throwable ignore) {
|
} catch (Throwable ignore) {
|
||||||
spec = tryExtractContextSpec(alg);
|
spec = tryExtractContextSpec(alg);
|
||||||
}
|
}
|
||||||
if (spec == null) {
|
if (spec == null) {
|
||||||
|
System.out.println("...spec=null (skip)");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("runAgreement(" + alg.id() + ", " + spec.getClass().getSimpleName() + ")");
|
KeyPair alice = generateKeyPair(alg);
|
||||||
System.out.println(" Alice.public " + keyInfo("key", alice.getPublic()));
|
KeyPair bob = generateKeyPair(alg);
|
||||||
System.out.println(" Alice.private " + keyInfo("key", alice.getPrivate()));
|
|
||||||
System.out.println(" Bob.public " + keyInfo("key", bob.getPublic()));
|
|
||||||
System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate()));
|
|
||||||
|
|
||||||
// assertDhCompatible(alice.getPrivate(), bob.getPublic());
|
if (alice == null || bob == null) {
|
||||||
// assertDhCompatible(bob.getPrivate(), alice.getPublic());
|
System.out.println("...keypair=null (skip)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("...classic agreement roundtrip: algId=" + alg.id() + ", spec="
|
||||||
|
+ spec.getClass().getSimpleName());
|
||||||
|
System.out.println("...Alice.public " + keyInfo("key", alice.getPublic()));
|
||||||
|
System.out.println("...Alice.private " + keyInfo("key", alice.getPrivate()));
|
||||||
|
System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
|
||||||
|
System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||||
|
|
||||||
|
// Optional DH sanity check (only when both sides are DH keys).
|
||||||
|
try {
|
||||||
|
assertDhCompatible(alice.getPrivate(), bob.getPublic());
|
||||||
|
assertDhCompatible(bob.getPrivate(), alice.getPublic());
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
// not DH, or provider-specific checks not applicable; continue anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
AgreementContext aCtx = null;
|
||||||
|
AgreementContext bCtx = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec);
|
||||||
|
bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec);
|
||||||
|
|
||||||
AgreementContext aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(),
|
|
||||||
spec);
|
|
||||||
AgreementContext bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
|
|
||||||
spec);
|
|
||||||
aCtx.setPeerPublic(bob.getPublic());
|
aCtx.setPeerPublic(bob.getPublic());
|
||||||
bCtx.setPeerPublic(alice.getPublic());
|
bCtx.setPeerPublic(alice.getPublic());
|
||||||
|
|
||||||
byte[] zA = aCtx.deriveSecret();
|
byte[] zA = aCtx.deriveSecret();
|
||||||
byte[] zB = bCtx.deriveSecret();
|
byte[] zB = bCtx.deriveSecret();
|
||||||
|
|
||||||
System.out.println(" Alice.secret =" + hex(zA));
|
System.out.println("...Alice.secret =" + hex(zA));
|
||||||
System.out.println(" Bob.secret =" + hex(zB));
|
System.out.println("...Bob.secret =" + hex(zB));
|
||||||
|
|
||||||
assertArrayEquals(zA, zB, alg.id() + ": DH/XDH secrets mismatch");
|
|
||||||
System.out.println("...ok");
|
|
||||||
|
|
||||||
|
assertArrayEquals(zA, zB, alg.id() + ": CLASSIC_AGREEMENT secrets mismatch");
|
||||||
|
System.out.println("...CLASSIC_AGREEMENT...ok");
|
||||||
|
} finally {
|
||||||
|
if (aCtx != null) {
|
||||||
try {
|
try {
|
||||||
aCtx.close();
|
aCtx.close();
|
||||||
} catch (IOException ignore) {
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (bCtx != null) {
|
||||||
try {
|
try {
|
||||||
bCtx.close();
|
bCtx.close();
|
||||||
} catch (IOException ignore) {
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do not break: a single algorithm may have multiple agreement capabilities
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logEnd();
|
logEnd();
|
||||||
@@ -271,19 +404,17 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
private static KeyPair generateKeyPair(CryptoAlgorithm alg) {
|
private static KeyPair generateKeyPair(CryptoAlgorithm alg) {
|
||||||
try {
|
try {
|
||||||
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
||||||
// System.out.println(" ...keyPair " + bi.getClass().getName());
|
|
||||||
if (bi.defaultKeySpec == null) {
|
if (bi.defaultKeySpec == null) {
|
||||||
// System.out.println(" ......skip default = null --> it was for keyImport");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
|
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
|
||||||
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(specType);
|
AsymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.asymmetricKeyBuilder(specType);
|
||||||
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
||||||
// System.out.println(" ......generated from " + spec);
|
return builder.generateKeyPair(spec);
|
||||||
return b.generateKeyPair(spec);
|
|
||||||
}
|
}
|
||||||
} catch (Throwable ignore) {
|
} catch (Throwable ignore) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -300,6 +431,7 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable ignore) {
|
} catch (Throwable ignore) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -315,15 +447,13 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
DHPrivateKey dhPriv = (DHPrivateKey) priv;
|
DHPrivateKey dhPriv = (DHPrivateKey) priv;
|
||||||
DHPublicKey dhPub = (DHPublicKey) pub;
|
DHPublicKey dhPub = (DHPublicKey) pub;
|
||||||
|
|
||||||
// 1) Parameter compatibility: same p,g and l-compatible (0 means "unspecified")
|
|
||||||
DHParameterSpec a = dhPriv.getParams();
|
DHParameterSpec a = dhPriv.getParams();
|
||||||
DHParameterSpec b = dhPub.getParams();
|
DHParameterSpec b = dhPub.getParams();
|
||||||
if (a == null || b == null) {
|
if (a == null || b == null) {
|
||||||
throw new InvalidKeyException("Missing DH parameters on one of the keys");
|
throw new InvalidKeyException("Missing DH parameters on one of the keys");
|
||||||
}
|
}
|
||||||
if (!a.getP().equals(b.getP()) || !a.getG().equals(b.getG())) {
|
if (!a.getP().equals(b.getP()) || !a.getG().equals(b.getG())) {
|
||||||
System.out.printf(" ...a.p=%s%n ...b.p=%s%n ...a.g=%s%n ...b.g=%s%n", a.getP(), b.getP(), a.getG(),
|
System.out.printf("...a.p=%s%n...b.p=%s%n...a.g=%s%n...b.g=%s%n", a.getP(), b.getP(), a.getG(), b.getG());
|
||||||
b.getG());
|
|
||||||
throw new InvalidKeyException("Incompatible DH parameters: (p,g) differ");
|
throw new InvalidKeyException("Incompatible DH parameters: (p,g) differ");
|
||||||
}
|
}
|
||||||
int la = a.getL();
|
int la = a.getL();
|
||||||
@@ -333,8 +463,6 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
"Incompatible DH parameters: private value length (l) differs: " + la + " vs " + lb);
|
"Incompatible DH parameters: private value length (l) differs: " + la + " vs " + lb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Public value Y sanity check: 2 <= Y <= p-2 (reject trivial/small subgroup
|
|
||||||
// values)
|
|
||||||
BigInteger p = a.getP();
|
BigInteger p = a.getP();
|
||||||
BigInteger y = dhPub.getY();
|
BigInteger y = dhPub.getY();
|
||||||
if (y == null) {
|
if (y == null) {
|
||||||
@@ -345,12 +473,10 @@ public class AgreementAlgorithmsRoundTripTest {
|
|||||||
throw new InvalidKeyException("DH public value Y out of range");
|
throw new InvalidKeyException("DH public value Y out of range");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Trial doPhase to prove the pair actually works with the provider
|
|
||||||
KeyAgreement ka = KeyAgreement.getInstance("DiffieHellman");
|
KeyAgreement ka = KeyAgreement.getInstance("DiffieHellman");
|
||||||
try {
|
try {
|
||||||
ka.init(priv);
|
ka.init(priv);
|
||||||
ka.doPhase(pub, true); // if this throws, they are not operationally compatible
|
ka.doPhase(pub, true);
|
||||||
// (we don't need the secret here; just proving doPhase succeeds)
|
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
throw new InvalidKeyException("KeyAgreement.doPhase failed: " + e.getMessage(), e);
|
throw new InvalidKeyException("KeyAgreement.doPhase failed: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|||||||
308
samples/src/test/java/demo/AgreementVariantsTest.java
Normal file
308
samples/src/test/java/demo/AgreementVariantsTest.java
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package demo;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import zeroecho.core.CryptoAlgorithm;
|
||||||
|
import zeroecho.core.CryptoAlgorithms;
|
||||||
|
import zeroecho.core.KeyUsage;
|
||||||
|
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||||
|
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
||||||
|
import zeroecho.core.alg.xdh.XdhSpec;
|
||||||
|
import zeroecho.core.context.AgreementContext;
|
||||||
|
import zeroecho.core.context.MessageAgreementContext;
|
||||||
|
import zeroecho.core.spec.VoidSpec;
|
||||||
|
import zeroecho.core.util.Strings;
|
||||||
|
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstration of agreement usage variants in ZeroEcho.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This sample illustrates three complementary models used in practice:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>KEM_ADAPTER</b> (example: ML-KEM / Kyber): the initiator is
|
||||||
|
* constructed with the recipient's {@link PublicKey} and produces an outbound
|
||||||
|
* encapsulation message; the responder is constructed with the matching
|
||||||
|
* {@link PrivateKey} and consumes that message.</li>
|
||||||
|
* <li><b>CLASSIC_AGREEMENT</b> (example: XDH / X25519): both parties hold their
|
||||||
|
* own {@link PrivateKey}, set the peer {@link PublicKey} explicitly, and derive
|
||||||
|
* the same raw shared secret.</li>
|
||||||
|
* <li><b>PAIR_MESSAGE</b> (example: XDH / X25519): both parties hold a key pair
|
||||||
|
* and exchange messages that are simply the X.509 SPKI encodings of their
|
||||||
|
* public keys. This yields a message-oriented handshake without changing the
|
||||||
|
* underlying agreement primitive.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Important note</h2>
|
||||||
|
* <p>
|
||||||
|
* All examples below produce a <em>raw</em> agreement secret (the direct output
|
||||||
|
* of KEM decapsulation or Diffie–Hellman agreement). Real protocols should feed
|
||||||
|
* the raw secret into a suitable KDF (typically HKDF) together with
|
||||||
|
* transcript/context info before using it as key material.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Note on resource management</h2>
|
||||||
|
* <p>
|
||||||
|
* The examples in this class intentionally do <em>not</em> use the
|
||||||
|
* {@code try-with-resources} construct when working with
|
||||||
|
* {@link zeroecho.core.context.AgreementContext} and
|
||||||
|
* {@link zeroecho.core.context.MessageAgreementContext}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Agreement contexts represent protocol-level state rather than traditional I/O
|
||||||
|
* resources. In real-world applications their lifecycle often spans multiple
|
||||||
|
* protocol steps (message send, receive, validation, key derivation) and may
|
||||||
|
* cross method or thread boundaries. Using explicit {@code try/finally} blocks
|
||||||
|
* in the examples makes this lifecycle visible and closer to how agreement
|
||||||
|
* contexts are typically managed in production code.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* In short-lived, fully synchronous scenarios (such as unit tests),
|
||||||
|
* {@code try-with-resources} is perfectly acceptable. It is omitted here purely
|
||||||
|
* for didactic reasons.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
class AgreementVariantsTest {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(AgreementVariantsTest.class.getName());
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setup() {
|
||||||
|
// Optional: activate BC/BCPQC if present.
|
||||||
|
// Keeps tests runnable even when providers are missing.
|
||||||
|
try {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KEM_ADAPTER example for ML-KEM (Kyber):
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This models a common "send one message, derive shared secret" pattern:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Initiator uses recipient {@link PublicKey} and produces an encapsulation
|
||||||
|
* message.</li>
|
||||||
|
* <li>Responder uses recipient {@link PrivateKey}, consumes the message, and
|
||||||
|
* derives the same secret.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void kemAdapter_mlKem_roundTrip() throws Exception {
|
||||||
|
LOG.info("kemAdapter_mlKem_roundTrip - KEM_ADAPTER (ML-KEM)");
|
||||||
|
|
||||||
|
KeyPair recipient = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024());
|
||||||
|
|
||||||
|
MessageAgreementContext initiator = null;
|
||||||
|
MessageAgreementContext responder = null;
|
||||||
|
try {
|
||||||
|
// Initiator: constructed with recipient's public key (encapsulation side).
|
||||||
|
initiator = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE);
|
||||||
|
|
||||||
|
// Responder: constructed with recipient's private key (decapsulation side).
|
||||||
|
responder = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(),
|
||||||
|
VoidSpec.INSTANCE);
|
||||||
|
|
||||||
|
// One-shot outbound message: KEM ciphertext / encapsulation payload.
|
||||||
|
byte[] enc = initiator.getPeerMessage();
|
||||||
|
// Responder consumes ciphertext to derive its secret.
|
||||||
|
responder.setPeerMessage(enc);
|
||||||
|
|
||||||
|
byte[] s1 = initiator.deriveSecret();
|
||||||
|
byte[] s2 = responder.deriveSecret();
|
||||||
|
|
||||||
|
LOG.log(Level.INFO, "KEM_ADAPTER: ciphertext={0}", Strings.toShortHexString(enc));
|
||||||
|
LOG.log(Level.INFO, "KEM_ADAPTER: initiatorSecret={0}", Strings.toShortHexString(s1));
|
||||||
|
LOG.log(Level.INFO, "KEM_ADAPTER: responderSecret={0}", Strings.toShortHexString(s2));
|
||||||
|
LOG.log(Level.INFO, "KEM_ADAPTER: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
|
||||||
|
} finally {
|
||||||
|
if (initiator != null) {
|
||||||
|
try {
|
||||||
|
initiator.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (responder != null) {
|
||||||
|
try {
|
||||||
|
responder.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLASSIC_AGREEMENT example for XDH/X25519:
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This is the traditional Diffie–Hellman model: both parties generate their own
|
||||||
|
* key pair, each side keeps a private key, and the peer public key is provided
|
||||||
|
* out-of-band (protocol message or session state).
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void classicAgreement_x25519_roundTrip() throws Exception {
|
||||||
|
LOG.info("classicAgreement_x25519_roundTrip - CLASSIC_AGREEMENT (X25519)");
|
||||||
|
|
||||||
|
CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh");
|
||||||
|
KeyPair alice = xdh.generateKeyPair();
|
||||||
|
KeyPair bob = xdh.generateKeyPair();
|
||||||
|
|
||||||
|
AgreementContext aCtx = null;
|
||||||
|
AgreementContext bCtx = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Both contexts are built from local private keys.
|
||||||
|
aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, alice.getPrivate(), XdhSpec.X25519);
|
||||||
|
bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bob.getPrivate(), XdhSpec.X25519);
|
||||||
|
|
||||||
|
// The protocol layer provides peer public keys (here we use in-memory
|
||||||
|
// exchange).
|
||||||
|
aCtx.setPeerPublic(bob.getPublic());
|
||||||
|
bCtx.setPeerPublic(alice.getPublic());
|
||||||
|
|
||||||
|
byte[] s1 = aCtx.deriveSecret();
|
||||||
|
byte[] s2 = bCtx.deriveSecret();
|
||||||
|
|
||||||
|
LOG.log(Level.INFO, "CLASSIC_AGREEMENT: aliceSecret={0}", Strings.toShortHexString(s1));
|
||||||
|
LOG.log(Level.INFO, "CLASSIC_AGREEMENT: bobSecret={0}", Strings.toShortHexString(s2));
|
||||||
|
LOG.log(Level.INFO, "CLASSIC_AGREEMENT: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
|
||||||
|
} finally {
|
||||||
|
if (aCtx != null) {
|
||||||
|
try {
|
||||||
|
aCtx.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bCtx != null) {
|
||||||
|
try {
|
||||||
|
bCtx.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PAIR_MESSAGE example for XDH/X25519:
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This demonstrates the "message-oriented" handshake for DH-style agreements.
|
||||||
|
* Each party holds a key pair and the outbound message is simply the local
|
||||||
|
* public key encoding (SPKI). The receiver imports the encoding and binds it as
|
||||||
|
* the peer key.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This model is particularly practical for protocol implementations because it
|
||||||
|
* makes the "to-be-sent" artifact explicit (a byte array message), similarly to
|
||||||
|
* KEM ciphertexts.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void pairMessage_x25519_roundTrip() throws Exception {
|
||||||
|
LOG.info("pairMessage_x25519_roundTrip - PAIR_MESSAGE (X25519)");
|
||||||
|
|
||||||
|
CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh");
|
||||||
|
KeyPair alice = xdh.generateKeyPair();
|
||||||
|
KeyPair bob = xdh.generateKeyPair();
|
||||||
|
|
||||||
|
// Wrapper is required because ZeroEcho capability dispatch uses Key (KeyPair is
|
||||||
|
// not a Key).
|
||||||
|
KeyPairKey aliceKey = new KeyPairKey(alice);
|
||||||
|
KeyPairKey bobKey = new KeyPairKey(bob);
|
||||||
|
|
||||||
|
MessageAgreementContext aCtx = null;
|
||||||
|
MessageAgreementContext bCtx = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, aliceKey, XdhSpec.X25519);
|
||||||
|
bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bobKey, XdhSpec.X25519);
|
||||||
|
|
||||||
|
// Outbound messages: SPKI encodings of local public keys.
|
||||||
|
byte[] aMsg = aCtx.getPeerMessage();
|
||||||
|
byte[] bMsg = bCtx.getPeerMessage();
|
||||||
|
|
||||||
|
LOG.log(Level.INFO, "PAIR_MESSAGE: aliceMsg={0}", Strings.toShortHexString(aMsg));
|
||||||
|
LOG.log(Level.INFO, "PAIR_MESSAGE: bobMsg={0}", Strings.toShortHexString(bMsg));
|
||||||
|
|
||||||
|
// Each side imports peer public key from message.
|
||||||
|
aCtx.setPeerMessage(bMsg);
|
||||||
|
bCtx.setPeerMessage(aMsg);
|
||||||
|
|
||||||
|
byte[] s1 = aCtx.deriveSecret();
|
||||||
|
byte[] s2 = bCtx.deriveSecret();
|
||||||
|
|
||||||
|
LOG.log(Level.INFO, "PAIR_MESSAGE: aliceSecret={0}", Strings.toShortHexString(s1));
|
||||||
|
LOG.log(Level.INFO, "PAIR_MESSAGE: bobSecret={0}", Strings.toShortHexString(s2));
|
||||||
|
LOG.log(Level.INFO, "PAIR_MESSAGE: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
|
||||||
|
} finally {
|
||||||
|
if (aCtx != null) {
|
||||||
|
try {
|
||||||
|
aCtx.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bCtx != null) {
|
||||||
|
try {
|
||||||
|
bCtx.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user