diff --git a/kse/src/main/java/org/kse/crypto/ecc/XDHCurves.java b/kse/src/main/java/org/kse/crypto/ecc/XDHCurves.java new file mode 100644 index 000000000..e55ffe8e9 --- /dev/null +++ b/kse/src/main/java/org/kse/crypto/ecc/XDHCurves.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004 - 2013 Wayne Grant + * 2013 - 2026 Kai Kramer + * + * This file is part of KeyStore Explorer. + * + * KeyStore Explorer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeyStore Explorer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeyStore Explorer. If not, see . + */ +package org.kse.crypto.ecc; + +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; + +/** + * Provide something like BC's SECNamedCurves/NISTNamedCurves/X962NamedCurves for X25519 and X448. + */ +public enum XDHCurves { + + X25519("X25519", EdECObjectIdentifiers.id_X25519, 256), + X448("X448", EdECObjectIdentifiers.id_X448, 456); + + private String jce; + private ASN1ObjectIdentifier oid; + private int bitLength; + + XDHCurves(String jce, ASN1ObjectIdentifier oid, int bitLength) { + this.jce = jce; + this.oid = oid; + this.bitLength = bitLength; + } + + public ASN1ObjectIdentifier oid() { + return oid; + } + + public String jce() { + return jce; + } + + public int bitLength() { + return bitLength; + } + + /** + * @return an enumeration containing the jce strings for curves contained in this structure. + */ + public static Enumeration getNames() { + return Collections.enumeration(Arrays.asList(X25519.jce, X448.jce)); + } + + public static XDHCurves resolve(ASN1ObjectIdentifier oid) { + return Arrays.stream(XDHCurves.values()).filter(e -> e.oid.equals(oid)).findFirst() + .orElseThrow(() -> new InvalidParameterException("Unknown OID: " + oid.getId())); + } +} diff --git a/kse/src/main/java/org/kse/crypto/jwk/JwkUtil.java b/kse/src/main/java/org/kse/crypto/jwk/JwkUtil.java index f424be15e..7ca3609af 100644 --- a/kse/src/main/java/org/kse/crypto/jwk/JwkUtil.java +++ b/kse/src/main/java/org/kse/crypto/jwk/JwkUtil.java @@ -38,6 +38,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.NamedParameterSpec; import java.security.spec.RSAPublicKeySpec; +import java.security.spec.XECPrivateKeySpec; import java.text.MessageFormat; import java.text.ParseException; import java.util.ArrayList; @@ -52,9 +53,10 @@ import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.jcajce.interfaces.EdDSAKey; import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; import org.bouncycastle.jcajce.interfaces.EdDSAPublicKey; +import org.bouncycastle.jcajce.interfaces.XDHPrivateKey; +import org.bouncycastle.jcajce.interfaces.XDHPublicKey; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.openssl.PEMException; @@ -120,8 +122,8 @@ public static String get(PublicKey publicKey, String alias) { jwkExporter = new RSAKeyExporter(publicKey); } else if (publicKey instanceof ECPublicKey) { jwkExporter = new ECKeyExporter(publicKey); - } else if (publicKey instanceof EdDSAPublicKey) { - jwkExporter = new EdDSAKeyExporter(publicKey); + } else if (publicKey instanceof EdDSAPublicKey || publicKey instanceof XDHPublicKey) { + jwkExporter = new EdKeyExporter(publicKey); } else { // Don't bother to translate this exception. This condition will never be encountered // since DExportPrivateKeyType.isJwkSupported and JwkPublicKeyExporter.isPublicKeyTypeExportable @@ -160,8 +162,8 @@ public static String get(PrivateKey privateKey, String alias, X509Certificate[] jwkExporter = new RSAKeyExporter(privateKey); } else if (privateKey instanceof ECPrivateKey) { jwkExporter = new ECKeyExporter(privateKey); - } else if (privateKey instanceof EdDSAPrivateKey) { - jwkExporter = new EdDSAKeyExporter(privateKey); + } else if (privateKey instanceof EdDSAPrivateKey || privateKey instanceof XDHPrivateKey) { + jwkExporter = new EdKeyExporter(privateKey); } else { // Don't bother to translate this exception. This condition will never be encountered // since DExportPrivateKeyType.isJwkSupported and JwkPublicKeyExporter.isPublicKeyTypeExportable @@ -332,10 +334,20 @@ public static PrivateKey toPrivateKey(JWK jwkKey) throws CryptoException { OctetKeyPair okp = jwkKey.toOctetKeyPair(); if (okp.getD() != null) { Curve curve = okp.getCurve(); - privateKey = KeyFactory.getInstance("EdDSA").generatePrivate( - new EdECPrivateKeySpec(new NamedParameterSpec(curve.getStdName()), okp.getDecodedD())); - // KSE uses the BC interfaces for EdDSA keys so convert BC - privateKey = Pkcs8Util.convert(privateKey); + if (Curve.Ed25519.equals(curve) || Curve.Ed448.equals(curve)) { + privateKey = KeyFactory.getInstance("EdDSA").generatePrivate( + new EdECPrivateKeySpec(new NamedParameterSpec(curve.getStdName()), okp.getDecodedD())); + // KSE uses the BC interfaces for EdDSA keys so convert BC + privateKey = Pkcs8Util.convert(privateKey); + } else if (Curve.X25519.equals(curve) || Curve.X448.equals(curve)) { + privateKey = KeyFactory.getInstance("XDH").generatePrivate( + new XECPrivateKeySpec(new NamedParameterSpec(curve.getStdName()), okp.getDecodedD())); + // KSE uses the BC interfaces for EdDSA keys so convert BC + privateKey = Pkcs8Util.convert(privateKey); + } else { + throw new CryptoException(MessageFormat + .format(res.getString("UnsupportedJwkKeyType.exception.message"), jwkKey.getKeyType())); + } } else { privateKey = null; } @@ -388,6 +400,8 @@ private static PublicKey toPublicKey(OctetKeyPair okp) throws CryptoException { ASN1ObjectIdentifier curveIdentifier = Curve.Ed25519.equals(crv) ? EdECObjectIdentifiers.id_Ed25519 : Curve.Ed448.equals(crv) ? EdECObjectIdentifiers.id_Ed448 : + Curve.X25519.equals(crv) ? EdECObjectIdentifiers.id_X25519 : + Curve.X448.equals(crv) ? EdECObjectIdentifiers.id_X448 : null; if (curveIdentifier == null) { @@ -413,6 +427,8 @@ public static boolean isPublicKeyTypeExportable(PublicKey publicKey) { switch (KeyPairUtil.getKeyPairType(publicKey)) { case ED448: case ED25519: + case X25519: + case X448: case RSA: return true; case EC: @@ -461,33 +477,42 @@ public static EncryptionType getEncryptionType(byte[] jwkData) { return null; } - private static class EdDSAKeyExporter implements JwkExporter { + private static class EdKeyExporter implements JwkExporter { protected static final Map SUPPORTED_CURVES = Map.of( KeyPairType.ED25519.jce(), Curve.Ed25519, - KeyPairType.ED448.jce(), Curve.Ed448 + KeyPairType.ED448.jce(), Curve.Ed448, + KeyPairType.X25519.jce(), Curve.X25519, + KeyPairType.X448.jce(), Curve.X448 ); - private final EdDSAPrivateKey privateKey; - private final EdDSAPublicKey publicKey; + private final PrivateKey privateKey; + private final PublicKey publicKey; - private EdDSAKeyExporter(PrivateKey privateKey) { - this.privateKey = (EdDSAPrivateKey) privateKey; - this.publicKey = this.privateKey.getPublicKey(); + private EdKeyExporter(PrivateKey privateKey) { + this.privateKey = privateKey; + if (privateKey instanceof EdDSAPrivateKey) { + publicKey = ((EdDSAPrivateKey) privateKey).getPublicKey(); + } else if (privateKey instanceof XDHPrivateKey) { + publicKey = ((XDHPrivateKey) privateKey).getPublicKey(); + } else { + // Should never get here, but in case, throw an exception + throw JwkExporterException.notSupported(privateKey.getAlgorithm(), null); + } } - private EdDSAKeyExporter(PublicKey publicKey) { + private EdKeyExporter(PublicKey publicKey) { this.privateKey = null; - this.publicKey = (EdDSAPublicKey) publicKey; + this.publicKey = publicKey; } - private Curve getCurve(EdDSAKey bcEdDSAPublicKey) { - return SUPPORTED_CURVES.get(bcEdDSAPublicKey.getAlgorithm()); + private Curve getCurve() { + return SUPPORTED_CURVES.get(publicKey.getAlgorithm()); } @Override public String export(String alias, X509Certificate[] chain) { - Curve curve = getCurve(publicKey); + Curve curve = getCurve(); try { SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance( publicKey.getEncoded()); diff --git a/kse/src/main/java/org/kse/crypto/keypair/KeyPairType.java b/kse/src/main/java/org/kse/crypto/keypair/KeyPairType.java index 59fbe01cf..eb572c0ac 100644 --- a/kse/src/main/java/org/kse/crypto/keypair/KeyPairType.java +++ b/kse/src/main/java/org/kse/crypto/keypair/KeyPairType.java @@ -37,6 +37,9 @@ public enum KeyPairType { ECGOST3410("ECGOST3410", "1.2.643.2.2.19", 256, 256, 0), ECGOST3410_2012("ECGOST3410-2012", "", 256, 512, 256), // There are two OIDs: One for 256-bit, one for 512-bit. + X25519("X25519", "1.3.101.110", 256, 256, 0), + X448("X448", "1.3.101.111", 456, 456, 0), + MLDSA44("ML-DSA-44", "2.16.840.1.101.3.4.3.17", 10_496, 10_496, 0), MLDSA65("ML-DSA-65", "2.16.840.1.101.3.4.3.18", 15_616, 15_616, 0), MLDSA87("ML-DSA-87", "2.16.840.1.101.3.4.3.19", 20_736, 20_736, 0), @@ -61,7 +64,8 @@ public enum KeyPairType { /** * Set of all EC key pair types (EC, ECDSA, ED25519, ED448) */ - public static final Set EC_TYPES_SET = EnumSet.of(EC, ECDSA, ED25519, ED448, ECGOST3410, ECGOST3410_2012); + public static final Set EC_TYPES_SET = EnumSet.of(EC, ECDSA, ED25519, ED448, X25519, X448, ECGOST3410, + ECGOST3410_2012); /** * Set of all ML-DSA key pair types @@ -183,6 +187,15 @@ public static boolean isSlhDsa(KeyPairType keyPairType) { return SLHDSA_TYPES_SET.contains(keyPairType); } + /** + * + * @param keyPairType The KeyPairType to check. + * @return True if keyPairType is a XDH key type. + */ + public static boolean isXDH(KeyPairType keyPairType) { + return X25519 == keyPairType || X448 == keyPairType; + } + /** * Gets the ECGOST key pair type for the given curve name. * diff --git a/kse/src/main/java/org/kse/crypto/keypair/KeyPairUtil.java b/kse/src/main/java/org/kse/crypto/keypair/KeyPairUtil.java index b76160aae..c445de9a2 100644 --- a/kse/src/main/java/org/kse/crypto/keypair/KeyPairUtil.java +++ b/kse/src/main/java/org/kse/crypto/keypair/KeyPairUtil.java @@ -22,6 +22,8 @@ import static org.kse.crypto.KeyType.ASYMMETRIC; import static org.kse.crypto.ecc.EdDSACurves.ED25519; import static org.kse.crypto.ecc.EdDSACurves.ED448; +import static org.kse.crypto.ecc.XDHCurves.X25519; +import static org.kse.crypto.ecc.XDHCurves.X448; import static org.kse.crypto.keypair.KeyPairType.DSA; import static org.kse.crypto.keypair.KeyPairType.EC; import static org.kse.crypto.keypair.KeyPairType.ECDSA; @@ -73,6 +75,7 @@ import org.kse.crypto.ecc.CurveSet; import org.kse.crypto.ecc.EccUtil; import org.kse.crypto.ecc.EdDSACurves; +import org.kse.crypto.ecc.XDHCurves; import org.kse.utilities.rng.RNG; /** @@ -139,7 +142,8 @@ public static KeyPair generateECKeyPair(String curveName, Provider provider) thr provider = KSE.BC; } - if (EdDSACurves.ED25519.jce().equals(curveName) || EdDSACurves.ED448.jce().equals(curveName)) { + if (EdDSACurves.ED25519.jce().equals(curveName) || EdDSACurves.ED448.jce().equals(curveName) + || XDHCurves.X25519.jce().equals(curveName) || XDHCurves.X448.jce().equals(curveName)) { keyPairGen = KeyPairGenerator.getInstance(curveName, provider); keyPairGen.initialize(new ECGenParameterSpec(curveName), RNG.newInstanceForLongLivedSecrets()); } else if (CurveSet.ECGOST.getAllCurveNames().contains(curveName)) { @@ -257,9 +261,9 @@ public static KeyInfo getKeyInfo(PublicKey publicKey) throws CryptoException { ECPublicKey pubk = (ECPublicKey) publicKey; int size = pubk.getParams().getOrder().bitLength(); return new KeyInfo(ASYMMETRIC, algorithm, size, EccUtil.getNamedCurve(publicKey)); - } else if (ED25519.jce().equalsIgnoreCase(algorithm)) { + } else if (ED25519.jce().equalsIgnoreCase(algorithm) || X25519.jce().equalsIgnoreCase(algorithm)) { return new KeyInfo(ASYMMETRIC, algorithm, ED25519.bitLength()); - } else if (ED448.jce().equalsIgnoreCase(algorithm)) { + } else if (ED448.jce().equalsIgnoreCase(algorithm) || X448.jce().equalsIgnoreCase(algorithm)) { return new KeyInfo(ASYMMETRIC, algorithm, ED448.bitLength()); } else if (ECGOST3410.jce().equalsIgnoreCase(algorithm) || ECGOST3410_2012.jce().equalsIgnoreCase(algorithm)) { // ECGOST parameters are ASN1Sequence so use ECNamedCurveSpec to get the curve name @@ -316,9 +320,9 @@ public static KeyInfo getKeyInfo(PrivateKey privateKey) throws CryptoException { ECParameterSpec spec = privk.getParams(); int size = spec.getOrder().bitLength(); return new KeyInfo(ASYMMETRIC, algorithm, size, EccUtil.getNamedCurve(privateKey)); - } else if (ED25519.jce().equalsIgnoreCase(algorithm)) { + } else if (ED25519.jce().equalsIgnoreCase(algorithm) || X25519.jce().equalsIgnoreCase(algorithm)) { return new KeyInfo(ASYMMETRIC, algorithm, ED25519.bitLength()); - } else if (ED448.jce().equalsIgnoreCase(algorithm)) { + } else if (ED448.jce().equalsIgnoreCase(algorithm) || X448.jce().equalsIgnoreCase(algorithm)) { return new KeyInfo(ASYMMETRIC, algorithm, ED448.bitLength()); } else if (isMlDSA(getKeyPairType(privateKey)) || isMlKEM(getKeyPairType(privateKey)) || isSlhDsa(getKeyPairType(privateKey))) { diff --git a/kse/src/main/java/org/kse/crypto/privatekey/Pkcs8Util.java b/kse/src/main/java/org/kse/crypto/privatekey/Pkcs8Util.java index 4c54394ee..d8618d449 100644 --- a/kse/src/main/java/org/kse/crypto/privatekey/Pkcs8Util.java +++ b/kse/src/main/java/org/kse/crypto/privatekey/Pkcs8Util.java @@ -31,6 +31,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.interfaces.EdECPrivateKey; +import java.security.interfaces.XECPrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.text.MessageFormat; @@ -489,7 +490,7 @@ public static PrivateKey convert(PrivateKey privateKey) throws CryptoException { // to the NamedPKCS8Key class for testing instanceof, but this hack is more // efficient at the cost of correctness. if ("sun.security.pkcs.NamedPKCS8Key".equals(privateKey.getClass().getName()) - || privateKey instanceof EdECPrivateKey) { + || privateKey instanceof EdECPrivateKey || privateKey instanceof XECPrivateKey) { try { // Shortest way to convert to a BC EdDSA key. Doesn't require importing any diff --git a/kse/src/main/java/org/kse/gui/KseFrame.java b/kse/src/main/java/org/kse/gui/KseFrame.java index 3d9589f3e..e9e36a4ff 100644 --- a/kse/src/main/java/org/kse/gui/KseFrame.java +++ b/kse/src/main/java/org/kse/gui/KseFrame.java @@ -2699,7 +2699,8 @@ private void showKeyStoreContextMenu(JTable jtKeyStore, int row, int x, int y) { // Not possible to sign if there is a keystore exception. } - boolean isSignAvailable = publicKey != null && !KeyPairType.isMlKEM(KeyPairUtil.getKeyPairType(publicKey)); + boolean isSignAvailable = publicKey != null && !KeyPairType.isMlKEM(KeyPairUtil.getKeyPairType(publicKey)) + && !KeyPairType.isXDH(KeyPairUtil.getKeyPairType(publicKey)); if (isSignAvailable) { jmiKeyPairGenerateCsr.setEnabled(true); jmiKeyPairGenerateCsr.setToolTipText(null); @@ -2713,7 +2714,7 @@ private void showKeyStoreContextMenu(JTable jtKeyStore, int row, int x, int y) { signJwtAction.setEnabled(signJwtAction.isKeySupported(publicKey)); jmiKeyPairSignJwt.setToolTipText(signJwtAction.getToolTip()); } else { - // Cannot request PKCS#10 CSR for ML-KEM since a signature is required for proof of possession + // Cannot request PKCS#10 CSR for ML-KEM or XDH since a signature is required for proof of possession // Generating a CSR can be re-enabled once CRMF is supported jmiKeyPairGenerateCsr.setEnabled(false); jmiKeyPairGenerateCsr.setToolTipText(res.getString("KseFrame.jmiKeyPairGenerateCsr.tooltip")); diff --git a/kse/src/main/java/org/kse/gui/dialogs/DGenerateKeyPair.java b/kse/src/main/java/org/kse/gui/dialogs/DGenerateKeyPair.java index b2412b60c..913a1b341 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/DGenerateKeyPair.java +++ b/kse/src/main/java/org/kse/gui/dialogs/DGenerateKeyPair.java @@ -48,6 +48,7 @@ import org.kse.crypto.ecc.CurveSet; import org.kse.crypto.ecc.EccUtil; import org.kse.crypto.ecc.EdDSACurves; +import org.kse.crypto.ecc.XDHCurves; import org.kse.crypto.keypair.KeyPairType; import org.kse.crypto.keystore.KeyStoreType; import org.kse.gui.MiGUtil; @@ -82,6 +83,7 @@ public class DGenerateKeyPair extends JEscDialog { private JRadioButton jrbEC; private JRadioButton jrbEdDSA; + private JRadioButton jrbXDH; private JLabel jlECCurveSet; private JComboBox jcbECCurveSet; private JLabel jlECCurve; @@ -90,6 +92,9 @@ public class DGenerateKeyPair extends JEscDialog { private JLabel jlEdDSACurve; private JComboBox jcbEdDSACurve; + private JLabel jlXDHCurve; + private JComboBox jcbXDHCurve; + private MLDSAKeySelector mldsaKeySelector; private MLKEMKeySelector mlkemKeySelector; private SlhDsaKeySelector slhDsaKeySelector; @@ -169,11 +174,20 @@ private void initComponents() { PlatformUtil.setMnemonic(jrbEdDSA, res.getString("DGenerateKeyPair.jrbEdDSA.mnemonic").charAt(0)); jrbEdDSA.setToolTipText(res.getString("DGenerateKeyPair.jrbEdDSA.tooltip")); + jrbXDH = new JRadioButton(res.getString("DGenerateKeyPair.jrbXDH.text"), false); + PlatformUtil.setMnemonic(jrbXDH, res.getString("DGenerateKeyPair.jrbXDH.mnemonic").charAt(0)); + jrbXDH.setToolTipText(res.getString("DGenerateKeyPair.jrbXDH.tooltip")); + if (isSelfSigned) { + jrbXDH.setEnabled(false); + jrbXDH.setToolTipText(res.getString("DGenerateKeyPair.jrbXDH.na.tooltip")); + } + ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(jrbRSA); buttonGroup.add(jrbDSA); buttonGroup.add(jrbEC); buttonGroup.add(jrbEdDSA); + buttonGroup.add(jrbXDH); jlECCurveSet = new JLabel(res.getString("DGenerateKeyPair.jlECCurveSet.text")); jlECCurveSet.setToolTipText(res.getString("DGenerateKeyPair.jlECCurveSet.tooltip")); @@ -196,7 +210,7 @@ private void initComponents() { jlEdDSACurve = new JLabel(res.getString("DGenerateKeyPair.jlEdDSACurve.text")); jlEdDSACurve.setDisplayedMnemonic(res.getString("DGenerateKeyPair.jlEdDSACurve.mnemonic").charAt(0)); - String[] edDsaCurveNames = Collections.list(EdDSACurves.getNames()).toArray(new String[0]); + String[] edDsaCurveNames = Collections.list(EdDSACurves.getNames()).toArray(String[]::new); for (int i = 0; i < edDsaCurveNames.length; i++) { KeyPairType type = KeyPairType.resolveJce(edDsaCurveNames[i]); edDsaCurveNames[i] = DialogHelper.formatNameWithSize(edDsaCurveNames[i], type.maxSize()); @@ -205,6 +219,18 @@ private void initComponents() { jcbEdDSACurve.setToolTipText(res.getString("DGenerateKeyPair.jcbEdDSACurve.tooltip")); jlEdDSACurve.setLabelFor(jcbEdDSACurve); + jlXDHCurve = new JLabel(res.getString("DGenerateKeyPair.jlXDHCurve.text")); + jlXDHCurve.setDisplayedMnemonic(res.getString("DGenerateKeyPair.jlXDHCurve.mnemonic").charAt(0)); + + String[] xdhCurveNames = Collections.list(XDHCurves.getNames()).toArray(String[]::new); + for (int i = 0; i < xdhCurveNames.length; i++) { + KeyPairType type = KeyPairType.resolveJce(xdhCurveNames[i]); + xdhCurveNames[i] = DialogHelper.formatNameWithSize(xdhCurveNames[i], type.maxSize()); + } + jcbXDHCurve = new JComboBox<>(xdhCurveNames); + jcbXDHCurve.setToolTipText(res.getString("DGenerateKeyPair.jcbXDHCurve.tooltip")); + jlXDHCurve.setLabelFor(jcbXDHCurve); + jbCancel = new JButton(res.getString("DGenerateKeyPair.jbCancel.text")); jbCancel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CANCEL_KEY); @@ -275,6 +301,11 @@ private void initComponents() { enableDisableElements(); } }); + jrbXDH.addItemListener(evt -> { + if (jrbXDH.isSelected()) { + enableDisableElements(); + } + }); enableDisableElements(); @@ -339,6 +370,12 @@ private void focusKeyPair(JTabbedPane jtpAlgorithms) { keyPairType == KeyPairType.ED25519 ? EdDSACurves.ED25519.jce() : EdDSACurves.ED448.jce(), keyPairType.maxSize())); jtpAlgorithms.setSelectedIndex(1); + } else if (keyPairType == KeyPairType.X25519 || keyPairType == KeyPairType.X448) { + jrbXDH.setSelected(true); + jcbXDHCurve.setSelectedItem(DialogHelper.formatNameWithSize( + keyPairType == KeyPairType.X25519 ? XDHCurves.X25519.jce() : XDHCurves.X448.jce(), + keyPairType.maxSize())); + jtpAlgorithms.setSelectedIndex(1); } else if (KeyPairType.isMlDSA(keyPairType)) { mldsaKeySelector.setSelected(true); jtpAlgorithms.setSelectedIndex(2); @@ -390,6 +427,9 @@ private JPanel createEcPanel() { jpEc.add(jrbEdDSA, ""); jpEc.add(jlEdDSACurve, ""); jpEc.add(jcbEdDSACurve, "growx, wrap"); + jpEc.add(jrbXDH, ""); + jpEc.add(jlXDHCurve, ""); + jpEc.add(jcbXDHCurve, "growx, wrap"); return jpEc; } @@ -503,6 +543,7 @@ protected void enableDisableElements() { boolean isEcSelected = jrbEC.isSelected(); boolean isEdDsaSelected = jrbEdDSA.isSelected(); + boolean isXDHSelected = jrbXDH.isSelected(); jlECCurve.setEnabled(isEcSelected); jcbECCurve.setEnabled(isEcSelected); @@ -512,6 +553,9 @@ protected void enableDisableElements() { jlEdDSACurve.setEnabled(isEdDsaSelected); jcbEdDSACurve.setEnabled(isEdDsaSelected); + jlXDHCurve.setEnabled(isXDHSelected); + jcbXDHCurve.setEnabled(isXDHSelected); + // Selectors manage their own internal state mldsaKeySelector.enableDisableElements(); mlkemKeySelector.enableDisableElements(); @@ -610,6 +654,8 @@ public String getCurveSet() { public String getCurveName() { if (jrbEdDSA.isSelected()) { return DialogHelper.extractNameFromFormatted((String) jcbEdDSACurve.getModel().getSelectedItem()); + } else if (jrbXDH.isSelected()) { + return DialogHelper.extractNameFromFormatted((String) jcbXDHCurve.getModel().getSelectedItem()); } String selectedItem = (String) jcbECCurve.getModel().getSelectedItem(); return extractCurveNameFromFormatted(selectedItem); @@ -666,6 +712,16 @@ public KeyPairType getKeyPairType() { } } + if (jrbXDH.isSelected()) { + String selectedCurve = DialogHelper + .extractNameFromFormatted((String) jcbXDHCurve.getModel().getSelectedItem()); + if (XDHCurves.X25519.jce().equals(selectedCurve)) { + return KeyPairType.X25519; + } else { + return KeyPairType.X448; + } + } + return KeyPairType.EC; } diff --git a/kse/src/main/java/org/kse/gui/dialogs/DViewAsymmetricKeyFields.java b/kse/src/main/java/org/kse/gui/dialogs/DViewAsymmetricKeyFields.java index 823d0bdaf..fc35bfd8b 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/DViewAsymmetricKeyFields.java +++ b/kse/src/main/java/org/kse/gui/dialogs/DViewAsymmetricKeyFields.java @@ -68,6 +68,8 @@ import org.bouncycastle.jcajce.interfaces.MLKEMPublicKey; import org.bouncycastle.jcajce.interfaces.SLHDSAPrivateKey; import org.bouncycastle.jcajce.interfaces.SLHDSAPublicKey; +import org.bouncycastle.jcajce.interfaces.XDHPrivateKey; +import org.bouncycastle.jcajce.interfaces.XDHPublicKey; import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters; @@ -133,9 +135,13 @@ private static String getTitle(Key key) { } else if (key instanceof ECPrivateKey) { return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PrivateKey.title"), "EC"); } else if (key instanceof EdDSAPublicKey) { - return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PublicKey.title"), getEdAlg(key)); + return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PublicKey.title"), key.getAlgorithm()); } else if (key instanceof EdDSAPrivateKey) { - return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PrivateKey.title"), getEdAlg(key)); + return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PrivateKey.title"), key.getAlgorithm()); + } else if (key instanceof XDHPublicKey) { + return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PublicKey.title"), key.getAlgorithm()); + } else if (key instanceof XDHPrivateKey) { + return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PrivateKey.title"), key.getAlgorithm()); } else if (key instanceof MLDSAPublicKey) { return MessageFormat.format(res.getString("DViewAsymmetricKeyFields.PublicKey.title"), "ML-DSA"); } else if (key instanceof MLDSAPrivateKey) { @@ -152,21 +158,6 @@ private static String getTitle(Key key) { throw new IllegalArgumentException("Unsupported key format for asymmetric fields viewer"); } - private static String getEdAlg(Key key) { - // Ed25519 or Ed448? - String edAlg; - if (key instanceof EdDSAPublicKey) { - EdDSAPublicKey bcEdDSAPublicKey = (EdDSAPublicKey) key; - edAlg = bcEdDSAPublicKey.getAlgorithm(); // Ed25519 or Ed448 - } else if (key instanceof EdDSAPrivateKey) { - EdDSAPrivateKey edPrivateKey = (EdDSAPrivateKey) key; - edAlg = edPrivateKey.getAlgorithm(); - } else { - throw new IllegalArgumentException("Unsupported key format for asymmetric fields viewer"); - } - return edAlg; - } - private void initFields() throws IOException { jlFields = new JLabel(res.getString("DViewAsymmetricKeyFields.jlFields.text")); @@ -241,9 +232,9 @@ private void populateFields() throws IOException { fields = getEcPubFields(); } else if (key instanceof ECPrivateKey) { fields = getEcPrivateFields(); - } else if (key instanceof EdDSAPublicKey) { + } else if (key instanceof EdDSAPublicKey || key instanceof XDHPublicKey) { fields = getEdPubFields(); - } else if (key instanceof EdECPrivateKey) { + } else if (key instanceof EdECPrivateKey || key instanceof XDHPrivateKey) { fields = getEdPrivateFields(); } else if (key instanceof MLDSAPublicKey) { fields = getMLDSAPublicFields(); diff --git a/kse/src/main/java/org/kse/gui/dialogs/DViewPrivateKey.java b/kse/src/main/java/org/kse/gui/dialogs/DViewPrivateKey.java index 168b18ccb..d4aa3f64e 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/DViewPrivateKey.java +++ b/kse/src/main/java/org/kse/gui/dialogs/DViewPrivateKey.java @@ -53,6 +53,7 @@ import org.bouncycastle.jcajce.interfaces.MLDSAPrivateKey; import org.bouncycastle.jcajce.interfaces.MLKEMPrivateKey; import org.bouncycastle.jcajce.interfaces.SLHDSAPrivateKey; +import org.bouncycastle.jcajce.interfaces.XDHPrivateKey; import org.kse.KSE; import org.kse.crypto.CryptoException; import org.kse.crypto.KeyInfo; @@ -316,7 +317,7 @@ private void populateDialog() throws CryptoException { jbFields.setEnabled((privateKey instanceof RSAPrivateKey) || (privateKey instanceof DSAPrivateKey) || (privateKey instanceof ECPrivateKey) || (privateKey instanceof EdECPrivateKey) || (privateKey instanceof MLDSAPrivateKey) || (privateKey instanceof MLKEMPrivateKey) - || (privateKey instanceof SLHDSAPrivateKey)); + || (privateKey instanceof SLHDSAPrivateKey || privateKey instanceof XDHPrivateKey)); } private void pemEncodingPressed() { diff --git a/kse/src/main/java/org/kse/gui/dialogs/DViewPublicKey.java b/kse/src/main/java/org/kse/gui/dialogs/DViewPublicKey.java index 91171bad6..a3f1a65d8 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/DViewPublicKey.java +++ b/kse/src/main/java/org/kse/gui/dialogs/DViewPublicKey.java @@ -58,6 +58,7 @@ import org.bouncycastle.jcajce.interfaces.MLDSAPublicKey; import org.bouncycastle.jcajce.interfaces.MLKEMPublicKey; import org.bouncycastle.jcajce.interfaces.SLHDSAPublicKey; +import org.bouncycastle.jcajce.interfaces.XDHPublicKey; import org.kse.KSE; import org.kse.crypto.CryptoException; import org.kse.crypto.KeyInfo; @@ -302,7 +303,7 @@ private void populateDialog() throws CryptoException { jbFields.setEnabled((publicKey instanceof RSAPublicKey) || (publicKey instanceof DSAPublicKey) || (publicKey instanceof ECPublicKey) || (publicKey instanceof EdDSAPublicKey) || publicKey instanceof MLDSAPublicKey || publicKey instanceof MLKEMPublicKey - || publicKey instanceof SLHDSAPublicKey); + || publicKey instanceof SLHDSAPublicKey || publicKey instanceof XDHPublicKey); } private void exportPressed() { diff --git a/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportPrivateKeyType.java b/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportPrivateKeyType.java index 7f352c515..a671b3538 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportPrivateKeyType.java +++ b/kse/src/main/java/org/kse/gui/dialogs/importexport/DExportPrivateKeyType.java @@ -94,6 +94,8 @@ private boolean isJwkSupported() throws CryptoException { switch (keyPairType) { case ED448: case ED25519: + case X25519: + case X448: case RSA: return true; case EC: diff --git a/kse/src/main/java/org/kse/gui/dialogs/importexport/DImportKeyPair.java b/kse/src/main/java/org/kse/gui/dialogs/importexport/DImportKeyPair.java index 0109b230a..8099d8f64 100644 --- a/kse/src/main/java/org/kse/gui/dialogs/importexport/DImportKeyPair.java +++ b/kse/src/main/java/org/kse/gui/dialogs/importexport/DImportKeyPair.java @@ -895,8 +895,9 @@ private X509Certificate[] loadCertificates(PrivateKey privateKey) { String certificatePath = jtfCertificatePath.getText().trim(); if (certificatePath.isEmpty()) { - // ML-KEM certs cannot be self-signed -- so a cert cannot be generated - boolean isSelfSignPossible = privateKey != null && !KeyPairType.isMlKEM(KeyPairUtil.getKeyPairType(privateKey)); + // ML-KEM and XDH certs cannot be self-signed -- so a cert cannot be generated + boolean isSelfSignPossible = privateKey != null && !KeyPairType.isMlKEM(KeyPairUtil.getKeyPairType(privateKey)) + && !KeyPairType.isXDH(KeyPairUtil.getKeyPairType(privateKey)); if (!isSelfSignPossible) { JOptionPane.showMessageDialog(this, res.getString("DImportKeyPair.CertificateRequired.message"), getTitle(), JOptionPane.WARNING_MESSAGE); diff --git a/kse/src/main/resources/org/kse/gui/dialogs/resources.properties b/kse/src/main/resources/org/kse/gui/dialogs/resources.properties index 2fde723bb..ce7530c1c 100644 --- a/kse/src/main/resources/org/kse/gui/dialogs/resources.properties +++ b/kse/src/main/resources/org/kse/gui/dialogs/resources.properties @@ -103,13 +103,19 @@ DGenerateKeyPair.jbCancel.text=Cancel DGenerateKeyPair.jbOK.text=OK DGenerateKeyPair.jcbECCurve.tooltip=A named curve (standardized domain parameters defining an elliptic curve) DGenerateKeyPair.jcbECCurveSet.tooltip=Set of named curves defined in standard documents +DGenerateKeyPair.jcbEdDSACurve.tooltip=Edwards-curve variant +DGenerateKeyPair.jcbXDHCurve.tooltip=Edwards-curve variant DGenerateKeyPair.jlECCurve.text=Named Curve: DGenerateKeyPair.jlECCurve.tooltip=A named curve (standardized domain parameters defining an elliptic curve) DGenerateKeyPair.jlECCurve.mnemonic=N DGenerateKeyPair.jlECCurveSet.text=Set: DGenerateKeyPair.jlECCurveSet.tooltip=Set of named curves defined in standard documents DGenerateKeyPair.jlECCurveSet.mnemonic=t +DGenerateKeyPair.jlEdDSACurve.text=Named Curve: +DGenerateKeyPair.jlEdDSACurve.mnemonic=a DGenerateKeyPair.jlKeySize.text=Key Size: +DGenerateKeyPair.jlXDHCurve.text=Named Curve: +DGenerateKeyPair.jlXDHCurve.mnemonic=m DGenerateKeyPair.jpContent.text=Algorithm Selection DGenerateKeyPair.jrbDSA.mnemonic=D DGenerateKeyPair.jrbDSA.text=DSA @@ -117,12 +123,17 @@ DGenerateKeyPair.jrbDSA.tooltip=DSA (Digital Signature Algorithm) key pair DGenerateKeyPair.jrbEC.mnemonic=C DGenerateKeyPair.jrbEC.text=EC/ECDSA DGenerateKeyPair.jrbEC.tooltip=ECC (Elliptic Curve Cryptography) key pair +DGenerateKeyPair.jrbEdDSA.mnemonic=d +DGenerateKeyPair.jrbEdDSA.text=EdDSA +DGenerateKeyPair.jrbEdDSA.tooltip=Edwards-curve Digital Signature Algorithm DGenerateKeyPair.jrbRSA.mnemonic=R DGenerateKeyPair.jrbRSA.text=RSA DGenerateKeyPair.jrbRSA.tooltip=RSA (Rivest, Shamir, Adleman) key pair +DGenerateKeyPair.jrbXDH.mnemonic=X +DGenerateKeyPair.jrbXDH.text=XDH +DGenerateKeyPair.jrbXDH.tooltip=Edwards-curve Key Exchange Algorithm +DGenerateKeyPair.jrbXDH.na.tooltip=Edwards-curve Key Exchange Algorithm is only available when using Sign New Key Pair. DGenerateKeyPair.jsKeySize.tooltip=Key size in bits -DGenerateKeyPair.jlEdDSACurve.text=Named Curve: -DGenerateKeyPair.jlEdDSACurve.mnemonic=a DGenerateKeyPair.Tab.Standard=Standard DGenerateKeyPair.Tab.Standard.mnemonic=S DGenerateKeyPair.Tab.EC=Elliptic Curves @@ -133,10 +144,6 @@ DGenerateKeyPair.Separator.Standard=Standard Algorithms DGenerateKeyPair.Separator.EC=Elliptic Curve Algorithms DGenerateKeyPair.Separator.PQC=Post-Quantum Algorithms DGenerateKeyPair.Separator.EdwardsCurve=Edwards-curve -DGenerateKeyPair.jrbEdDSA.mnemonic=d -DGenerateKeyPair.jrbEdDSA.text=EdDSA -DGenerateKeyPair.jrbEdDSA.tooltip=Edwards-curve Digital Signature Algorithm -DGenerateKeyPair.jcbEdDSACurve.tooltip=Edwards-curve variant DGenerateKeyPair.jrbSizeManual.text=Size DGenerateKeyPair.jrbSizeManual.mnemonic=z DGenerateKeyPair.Size.2048=2048 bits diff --git a/kse/src/test/java/org/kse/crypto/KeyTestsBase.java b/kse/src/test/java/org/kse/crypto/KeyTestsBase.java index 25059f91a..0234f9791 100644 --- a/kse/src/test/java/org/kse/crypto/KeyTestsBase.java +++ b/kse/src/test/java/org/kse/crypto/KeyTestsBase.java @@ -39,6 +39,8 @@ import org.bouncycastle.jcajce.interfaces.MLKEMPublicKey; import org.bouncycastle.jcajce.interfaces.SLHDSAPrivateKey; import org.bouncycastle.jcajce.interfaces.SLHDSAPublicKey; +import org.bouncycastle.jcajce.interfaces.XDHPrivateKey; +import org.bouncycastle.jcajce.interfaces.XDHPublicKey; import org.junit.jupiter.api.BeforeAll; import org.kse.KSE; import org.kse.crypto.keypair.KeyPairType; @@ -60,6 +62,8 @@ public abstract class KeyTestsBase extends CryptoTestsBase { protected static ECPublicKey gostPublicKey; protected static EdDSAPrivateKey eddsaPrivateKey; protected static EdDSAPublicKey eddsaPublicKey; + protected static XDHPrivateKey xdhPrivateKey; + protected static XDHPublicKey xdhPublicKey; protected static MLDSAPublicKey mldsaPublicKey; /** * MLDSA private keys come with different types of encoding, but for now only the combined form is @@ -75,13 +79,13 @@ public abstract class KeyTestsBase extends CryptoTestsBase { protected static List privateKeys() { - return Arrays.asList(rsaPrivateKey, dsaPrivateKey, ecPrivateKey, gostPrivateKey, eddsaPrivateKey, + return Arrays.asList(rsaPrivateKey, dsaPrivateKey, ecPrivateKey, gostPrivateKey, eddsaPrivateKey, xdhPrivateKey, mldsaPrivateKeySeedAndExpanded, mlkemPrivateKey, slhDsaPrivateKey); } protected static List publicKeys() { - return Arrays.asList(rsaPublicKey, dsaPublicKey, ecPublicKey, gostPublicKey, eddsaPublicKey, mldsaPublicKey, - mlkemPublicKey, slhDsaPublicKey); + return Arrays.asList(rsaPublicKey, dsaPublicKey, ecPublicKey, gostPublicKey, eddsaPublicKey, xdhPublicKey, + mldsaPublicKey, mlkemPublicKey, slhDsaPublicKey); } @BeforeAll @@ -117,6 +121,12 @@ static void initKeys() throws CryptoException { eddsaPublicKey = (EdDSAPublicKey) eddsaKeyPair.getPublic(); } + if (xdhPrivateKey == null) { + KeyPair eddsaKeyPair = KeyPairUtil.generateECKeyPair("X25519", KSE.BC); + xdhPrivateKey = (XDHPrivateKey) eddsaKeyPair.getPrivate(); + xdhPublicKey = (XDHPublicKey) eddsaKeyPair.getPublic(); + } + if (mldsaPublicKey == null) { KeyPair mldsaKeyPair = KeyPairUtil.generateKeyPair(KeyPairType.MLDSA44, KSE.BC); mldsaPrivateKeySeedAndExpanded = (MLDSAPrivateKey) mldsaKeyPair.getPrivate(); diff --git a/kse/src/test/java/org/kse/crypto/jwk/JwkUtilTest.java b/kse/src/test/java/org/kse/crypto/jwk/JwkUtilTest.java index e82ce50e8..b5d73c845 100644 --- a/kse/src/test/java/org/kse/crypto/jwk/JwkUtilTest.java +++ b/kse/src/test/java/org/kse/crypto/jwk/JwkUtilTest.java @@ -55,6 +55,7 @@ import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; +import org.bouncycastle.jcajce.interfaces.XDHPrivateKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.BeforeEach; @@ -81,6 +82,12 @@ private byte[] encodePrivateKey(byte[] privateKeyScalar, String curve) throws IO case "Ed25519": oid = EdECObjectIdentifiers.id_Ed25519; break; + case "X448": + oid = EdECObjectIdentifiers.id_X448; + break; + case "X25519": + oid = EdECObjectIdentifiers.id_X25519; + break; default: throw new IllegalArgumentException(curve); } @@ -104,6 +111,14 @@ KeyPair createEdDSAPrivateKey(String name, byte[] d) throws Exception { EdDSAPrivateKey privateKey = (EdDSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(pkcs8EncodedKey)); return new KeyPair(privateKey.getPublicKey(), privateKey); } + KeyPair createXDHPrivateKey(String name, byte[] d) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + byte[] pkcs8EncodedKey = encodePrivateKey(d, name); + KeyFactory keyFactory = KeyFactory.getInstance(name, "BC"); + + XDHPrivateKey privateKey = (XDHPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(pkcs8EncodedKey)); + return new KeyPair(privateKey.getPublicKey(), privateKey); + } @Nested class ForEd25519{ byte[] d = Hex.decode("DCAAACEADDAC99D31020B81795E1F64CA36D81701034EE598C161F2655368F76"); @@ -301,6 +316,130 @@ void shouldExportPublicWithProvidedAlias() throws Exception { assertNull(jwk.getParsedX509CertChain()); } } + @Nested + class ForX25519{ + byte[] d = Hex.decode("DCAAACEADDAC99D31020B81795E1F64CA36D81701034EE598C161F2655368F76"); + KeyPair keyPair = createXDHPrivateKey("X25519", d); + + ForX25519() throws Exception {} + + private final String expectedJwkPub = + "{\n" + + " \"kty\": \"OKP\",\n" + + " \"kid\": \"alias\",\n" + + " \"crv\": \"X25519\",\n" + + " \"x\": \"P5xUKxUM0UlNnCpwxtOzJXdkwC4HHMsMTXcXxBmY3W4\"\n" + + "}"; + private final String expectedJwk = + "{\n" + + " \"kty\": \"OKP\",\n" + + " \"kid\": \"alias\",\n" + + " \"crv\": \"X25519\",\n" + + " \"x\": \"P5xUKxUM0UlNnCpwxtOzJXdkwC4HHMsMTXcXxBmY3W4\",\n" + + " \"d\": \"3Kqs6t2smdMQILgXleH2TKNtgXAQNO5ZjBYfJlU2j3Y\"\n" + + "}"; + @Test + void shouldExportWithProvidedAlias() throws Exception { + String actualJwk = JwkUtil.get(keyPair.getPrivate(), "alias"); + JSONAssert.assertEquals(expectedJwk, actualJwk, JSONCompareMode.STRICT); + + JWK jwk = JwkUtil.load(actualJwk.getBytes()); + PrivateKey actualPrivateKey = JwkUtil.toPrivateKey(jwk); + PublicKey actualPublicKey = JwkUtil.toPublicKey(jwk); + assertArrayEquals(keyPair.getPrivate().getEncoded(), actualPrivateKey.getEncoded()); + assertArrayEquals(keyPair.getPublic().getEncoded(), actualPublicKey.getEncoded()); + assertNull(jwk.getParsedX509CertChain()); + } + @Test + void shouldExportAndGenerateAliasIfNotProvided() throws Exception { + String actualJwk = JwkUtil.get(keyPair.getPrivate(), null); + JSONAssert.assertEquals( + expectedJwk.replace("alias", "k2iMCMk9Jm9rHmTkBlzk_0UpOmC3FhQsv5RBUjYBqBA"), + actualJwk, + JSONCompareMode.STRICT); + + JWK jwk = JwkUtil.load(actualJwk.getBytes()); + PrivateKey actualPrivateKey = JwkUtil.toPrivateKey(jwk); + PublicKey actualPublicKey = JwkUtil.toPublicKey(jwk); + assertArrayEquals(keyPair.getPrivate().getEncoded(), actualPrivateKey.getEncoded()); + assertArrayEquals(keyPair.getPublic().getEncoded(), actualPublicKey.getEncoded()); + assertNull(jwk.getParsedX509CertChain()); + } + @Test + void shouldExportPublicWithProvidedAlias() throws Exception { + String actualJwk = JwkUtil.get(keyPair.getPublic(), "alias"); + JSONAssert.assertEquals(expectedJwkPub, actualJwk, JSONCompareMode.STRICT); + + JWK jwk = JwkUtil.load(actualJwk.getBytes()); + PrivateKey actualPrivateKey = JwkUtil.toPrivateKey(jwk); + PublicKey actualPublicKey = JwkUtil.toPublicKey(jwk); + assertNull(actualPrivateKey); + assertArrayEquals(keyPair.getPublic().getEncoded(), actualPublicKey.getEncoded()); + assertNull(jwk.getParsedX509CertChain()); + } + } + @Nested + class ForX448{ + byte[] d = Hex.decode("0DAAE3F2A4AC597FD67BEE8F46F50AE24CD67D53F846BE5DEB6ACE0CB67FD9EA95604FBDEF9F566A4245C9A5BAB81FD8DC0AF3179CE94C40"); + KeyPair keyPair = createXDHPrivateKey("X448", d); + + ForX448() throws Exception {} + + private final String expectedJwkPub = + "{\n" + + " \"kty\": \"OKP\",\n" + + " \"kid\": \"alias\",\n" + + " \"crv\": \"X448\",\n" + + " \"x\": \"c_Bns0IeTg2fax-1_NNzqKHHFE0WRordmvumcuD7-58gMAxNq2QGT59nRwc1a7VduRJF0JJ1vIc\"\n" + + "}"; + private final String expectedJwk = + "{\n" + + " \"kty\": \"OKP\",\n" + + " \"kid\": \"alias\",\n" + + " \"crv\": \"X448\",\n" + + " \"x\": \"c_Bns0IeTg2fax-1_NNzqKHHFE0WRordmvumcuD7-58gMAxNq2QGT59nRwc1a7VduRJF0JJ1vIc\",\n" + + " \"d\": \"Darj8qSsWX_We-6PRvUK4kzWfVP4Rr5d62rODLZ_2eqVYE-9759WakJFyaW6uB_Y3ArzF5zpTEA\"\n" + + "}"; + @Test + void shouldExportWithProvidedAlias() throws Exception { + String actualJwk = JwkUtil.get(keyPair.getPrivate(), "alias"); + JSONAssert.assertEquals(expectedJwk, actualJwk, JSONCompareMode.STRICT); + + JWK jwk = JwkUtil.load(actualJwk.getBytes()); + PrivateKey actualPrivateKey = JwkUtil.toPrivateKey(jwk); + PublicKey actualPublicKey = JwkUtil.toPublicKey(jwk); + assertArrayEquals(keyPair.getPrivate().getEncoded(), actualPrivateKey.getEncoded()); + assertArrayEquals(keyPair.getPublic().getEncoded(), actualPublicKey.getEncoded()); + assertNull(jwk.getParsedX509CertChain()); + } + @Test + void shouldExportAndGenerateAliasIfNotProvided() throws Exception { + String actualJwk = JwkUtil.get(keyPair.getPrivate(), null); + JSONAssert.assertEquals( + expectedJwk.replace("alias", "dClFYxfmFbJU4onZvadO2pgEMu5n4Q1r3A1lAPnBMa8"), + actualJwk, + JSONCompareMode.STRICT); + + JWK jwk = JwkUtil.load(actualJwk.getBytes()); + PrivateKey actualPrivateKey = JwkUtil.toPrivateKey(jwk); + PublicKey actualPublicKey = JwkUtil.toPublicKey(jwk); + assertArrayEquals(keyPair.getPrivate().getEncoded(), actualPrivateKey.getEncoded()); + assertArrayEquals(keyPair.getPublic().getEncoded(), actualPublicKey.getEncoded()); + assertNull(jwk.getParsedX509CertChain()); + } + @Test + void shouldExportPublicWithProvidedAlias() throws Exception { + String actualJwk = JwkUtil.get(keyPair.getPublic(), "alias"); + JSONAssert.assertEquals(expectedJwkPub, actualJwk, JSONCompareMode.STRICT); + + JWK jwk = JwkUtil.load(actualJwk.getBytes()); + PrivateKey actualPrivateKey = JwkUtil.toPrivateKey(jwk); + PublicKey actualPublicKey = JwkUtil.toPublicKey(jwk); + assertNull(actualPrivateKey); + assertArrayEquals(keyPair.getPublic().getEncoded(), actualPublicKey.getEncoded()); + assertNull(jwk.getParsedX509CertChain()); + } + } @Test void handlesAttemptToExportUnsupportedEdDSAKeyWithGrace() {