public static byte[] ecdh(XECPrivateKey privateKey, XECPublicKey publicKey) {
        try {
            var keyAgreement = KeyAgreement.getInstance("XDH");
            keyAgreement.init(privateKey);
            keyAgreement.doPhase(publicKey, true);
            byte[] sharedSecret = keyAgreement.generateSecret();
            // RFC 7748 recommends checking that the shared secret is not all zero bytes.
            // Furthermore, RFC 9180 states "For X25519 and X448, public keys and Diffie-Hellman
            // outputs MUST be validated as described in [RFC7748]".
            // Usually we won't get here at all since Java will throw an InvalidKeyException
            // from detecting a key with a low order point. But in case we _do_ get here, fail fast.
            if (SideChannelSafe.allZeros(sharedSecret)) {
                throw new IllegalArgumentException("Computed shared secret is all zeroes");
            }
            return sharedSecret;
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException(e);
        }
    }
@Test
    void x25519_ecdh_matches_rfc_7748_test_vector() {
        var alice_priv = xecPrivFromHex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a");
        var alice_pub  = xecPubFromHex( "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a");
        var bob_priv   = xecPrivFromHex("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb");
        var bob_public = xecPubFromHex( "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");

        var expectedShared = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";

        byte[] sharedAliceToBob = KeyUtils.ecdh(alice_priv, bob_public);
        assertEquals(expectedShared, hex(sharedAliceToBob));

        byte[] sharedBobToAlice = KeyUtils.ecdh(bob_priv, alice_pub);
        assertEquals(expectedShared, hex(sharedBobToAlice));
    }