Test request serialization

Signed-off-by: Luca Fulchir <luca.fulchir@runesauth.com>
This commit is contained in:
Luca Fulchir 2023-06-09 19:06:58 +02:00
parent 55e10a60c6
commit 5625bd95a4
Signed by: luca.fulchir
GPG Key ID: 8F6440603D13A78E
7 changed files with 205 additions and 63 deletions

View File

@ -5,7 +5,7 @@ use ::zeroize::Zeroize;
/// User identifier. 16 bytes for easy uuid conversion /// User identifier. 16 bytes for easy uuid conversion
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct UserID([u8; 16]); pub struct UserID(pub [u8; 16]);
impl From<[u8; 16]> for UserID { impl From<[u8; 16]> for UserID {
fn from(raw: [u8; 16]) -> Self { fn from(raw: [u8; 16]) -> Self {
@ -36,7 +36,7 @@ impl UserID {
/// Authentication Token, basically just 32 random bytes /// Authentication Token, basically just 32 random bytes
#[derive(Clone, Zeroize)] #[derive(Clone, Zeroize)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct Token([u8; 32]); pub struct Token(pub [u8; 32]);
impl Token { impl Token {
/// New random token, anonymous should not check this anyway /// New random token, anonymous should not check this anyway
@ -110,7 +110,7 @@ impl Domain {
pub const SERVICEID_AUTH: ServiceID = ServiceID([0; 16]); pub const SERVICEID_AUTH: ServiceID = ServiceID([0; 16]);
/// The Service ID is a UUID associated with the service. /// The Service ID is a UUID associated with the service.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct ServiceID([u8; 16]); pub struct ServiceID(pub [u8; 16]);
impl From<[u8; 16]> for ServiceID { impl From<[u8; 16]> for ServiceID {
fn from(raw: [u8; 16]) -> Self { fn from(raw: [u8; 16]) -> Self {

View File

@ -97,7 +97,9 @@ pub struct Req {
pub exchange_key: ExchangePubKey, pub exchange_key: ExchangePubKey,
/// encrypted data /// encrypted data
pub data: ReqInner, pub data: ReqInner,
// Security: Add padding to min: 1200 bytes to avoid amplification attaks // SECURITY: TODO: Add padding to min: 1200 bytes
// to avoid amplification attaks
// also: 1200 < 1280 to allow better vpn compatibility
} }
impl Req { impl Req {
@ -125,7 +127,9 @@ impl Req {
+ HkdfKind::len() + HkdfKind::len()
+ CipherKind::len() + CipherKind::len()
+ self.exchange_key.kind().pub_len() + self.exchange_key.kind().pub_len()
+ self.cipher.nonce_len().0
+ self.data.len() + self.data.len()
+ self.cipher.tag_len().0
} }
/// Serialize into raw bytes /// Serialize into raw bytes
/// NOTE: assumes that there is exactly as much buffer as needed /// NOTE: assumes that there is exactly as much buffer as needed
@ -135,8 +139,21 @@ impl Req {
tag_len: TagLen, tag_len: TagLen,
out: &mut [u8], out: &mut [u8],
) { ) {
//assert!(out.len() > , ": not enough buffer to serialize"); out[0..2].copy_from_slice(&self.key_id.0.to_le_bytes());
todo!() out[2] = self.exchange as u8;
out[3] = self.hkdf as u8;
out[4] = self.cipher as u8;
let key_len = self.exchange_key.len();
let written_next = 5 + key_len;
self.exchange_key.serialize_into(&mut out[5..written_next]);
let written = written_next;
if let ReqInner::ClearText(data) = &self.data {
let from = written + head_len.0;
let to = out.len() - tag_len.0;
data.serialize(&mut out[from..to]);
} else {
unreachable!();
}
} }
} }
@ -147,7 +164,7 @@ impl super::HandshakeParsing for Req {
return Err(Error::NotEnoughData); return Err(Error::NotEnoughData);
} }
let key_id: KeyID = let key_id: KeyID =
KeyID(u16::from_le_bytes(raw[0..1].try_into().unwrap())); KeyID(u16::from_le_bytes(raw[0..2].try_into().unwrap()));
use ::num_traits::FromPrimitive; use ::num_traits::FromPrimitive;
let exchange: KeyExchangeKind = match KeyExchangeKind::from_u8(raw[2]) { let exchange: KeyExchangeKind = match KeyExchangeKind::from_u8(raw[2]) {
Some(exchange) => exchange, Some(exchange) => exchange,
@ -161,7 +178,7 @@ impl super::HandshakeParsing for Req {
Some(cipher) => cipher, Some(cipher) => cipher,
None => return Err(Error::Parsing), None => return Err(Error::Parsing),
}; };
let (exchange_key, len) = match ExchangePubKey::from_slice(&raw[5..]) { let (exchange_key, len) = match ExchangePubKey::deserialize(&raw[5..]) {
Ok(exchange_key) => exchange_key, Ok(exchange_key) => exchange_key,
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
}; };
@ -235,6 +252,21 @@ impl AuthInfo {
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
Self::MIN_PKT_LEN + self.domain.len() Self::MIN_PKT_LEN + self.domain.len()
} }
/// serialize into a buffer
/// Note: assumes there is enough space
pub fn serialize(&self, out: &mut [u8]) {
out[..auth::UserID::len()].copy_from_slice(&self.user.0);
const WRITTEN_TOKEN: usize = auth::UserID::len() + auth::Token::len();
out[auth::UserID::len()..WRITTEN_TOKEN].copy_from_slice(&self.token.0);
const WRITTEN_SERVICE_ID: usize =
WRITTEN_TOKEN + auth::ServiceID::len();
out[WRITTEN_TOKEN..WRITTEN_SERVICE_ID]
.copy_from_slice(&self.service_id.0);
let domain_len = self.domain.0.as_bytes().len() as u8;
out[WRITTEN_SERVICE_ID] = domain_len;
const WRITTEN_DOMAIN_LEN: usize = WRITTEN_SERVICE_ID + 1;
out[WRITTEN_DOMAIN_LEN..].copy_from_slice(&self.domain.0.as_bytes());
}
/// deserialize from raw bytes /// deserialize from raw bytes
pub fn deserialize(raw: &[u8]) -> Result<Self, Error> { pub fn deserialize(raw: &[u8]) -> Result<Self, Error> {
if raw.len() < Self::MIN_PKT_LEN { if raw.len() < Self::MIN_PKT_LEN {
@ -295,6 +327,19 @@ impl ReqData {
/// Minimum byte length of the request data /// Minimum byte length of the request data
pub const MIN_PKT_LEN: usize = pub const MIN_PKT_LEN: usize =
16 + KeyID::len() + ID::len() + AuthInfo::MIN_PKT_LEN; 16 + KeyID::len() + ID::len() + AuthInfo::MIN_PKT_LEN;
/// serialize into a buffer
/// Note: assumes there is enough space
pub fn serialize(&self, out: &mut [u8]) {
out[..Nonce::len()].copy_from_slice(&self.nonce.0);
const WRITTEN_KEY: usize = Nonce::len() + KeyID::len();
out[Nonce::len()..WRITTEN_KEY]
.copy_from_slice(&self.client_key_id.0.to_le_bytes());
const WRITTEN: usize = WRITTEN_KEY;
const WRITTEN_ID: usize = WRITTEN + 8;
out[WRITTEN..WRITTEN_ID]
.copy_from_slice(&self.id.as_u64().to_le_bytes());
self.auth.serialize(&mut out[WRITTEN_ID..]);
}
/// Parse the cleartext raw data /// Parse the cleartext raw data
pub fn deserialize(raw: &[u8]) -> Result<Self, Error> { pub fn deserialize(raw: &[u8]) -> Result<Self, Error> {
if raw.len() < Self::MIN_PKT_LEN { if raw.len() < Self::MIN_PKT_LEN {

View File

@ -1,6 +1,8 @@
//! Handhsake handling //! Handhsake handling
pub mod dirsync; pub mod dirsync;
#[cfg(test)]
mod tests;
use crate::{ use crate::{
auth::ServiceID, auth::ServiceID,
@ -194,7 +196,7 @@ impl HandshakeData {
/// Kind of handshake /// Kind of handshake
#[derive(::num_derive::FromPrimitive, Debug, Clone, Copy)] #[derive(::num_derive::FromPrimitive, Debug, Clone, Copy)]
#[repr(u8)] #[repr(u8)]
pub enum Kind { pub enum HandshakeKind {
/// 1-RTT, Directory synchronized handshake /// 1-RTT, Directory synchronized handshake
/// Request /// Request
DirSyncReq = 0, DirSyncReq = 0,
@ -210,6 +212,12 @@ pub enum Kind {
.... ....
*/ */
} }
impl HandshakeKind {
/// Length of the serialized field
pub const fn len() -> usize {
1
}
}
/// Parsed handshake /// Parsed handshake
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -230,7 +238,7 @@ impl Handshake {
} }
/// return the total length of the handshake /// return the total length of the handshake
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
ProtocolVersion::len() + self.data.len() ProtocolVersion::len() + HandshakeKind::len() + self.data.len()
} }
const MIN_PKT_LEN: usize = 8; const MIN_PKT_LEN: usize = 8;
/// Parse the packet and return the parsed handshake /// Parse the packet and return the parsed handshake
@ -242,13 +250,15 @@ impl Handshake {
Some(fenrir_version) => fenrir_version, Some(fenrir_version) => fenrir_version,
None => return Err(Error::Parsing), None => return Err(Error::Parsing),
}; };
let handshake_kind = match Kind::from_u8(raw[1]) { let handshake_kind = match HandshakeKind::from_u8(raw[1]) {
Some(handshake_kind) => handshake_kind, Some(handshake_kind) => handshake_kind,
None => return Err(Error::Parsing), None => return Err(Error::Parsing),
}; };
let data = match handshake_kind { let data = match handshake_kind {
Kind::DirSyncReq => dirsync::Req::deserialize(&raw[2..])?, HandshakeKind::DirSyncReq => dirsync::Req::deserialize(&raw[2..])?,
Kind::DirSyncResp => dirsync::Resp::deserialize(&raw[2..])?, HandshakeKind::DirSyncResp => {
dirsync::Resp::deserialize(&raw[2..])?
}
}; };
Ok(Self { Ok(Self {
fenrir_version, fenrir_version,
@ -263,9 +273,14 @@ impl Handshake {
tag_len: TagLen, tag_len: TagLen,
out: &mut [u8], out: &mut [u8],
) { ) {
assert!(out.len() > 1, "Handshake: not enough buffer to serialize"); out[0] = self.fenrir_version as u8;
self.fenrir_version.serialize(&mut out[0]); out[1] = match &self.data {
self.data.serialize(head_len, tag_len, &mut out[1..]); HandshakeData::DirSync(d) => match d {
dirsync::DirSync::Req(_) => HandshakeKind::DirSyncReq,
dirsync::DirSync::Resp(_) => HandshakeKind::DirSyncResp,
},
} as u8;
self.data.serialize(head_len, tag_len, &mut out[2..]);
} }
} }

View File

@ -0,0 +1,65 @@
use crate::{
auth,
connection::{handshake::*, ID},
enc,
};
#[test]
fn test_handshake_dirsync_req() {
let rand = enc::Random::new();
let secret = enc::Secret::new_rand(&rand);
let cipher_send = enc::sym::CipherSend::new(
enc::sym::CipherKind::XChaCha20Poly1305,
secret,
&rand,
);
let (_, exchange_key) =
match enc::asym::KeyExchangeKind::X25519DiffieHellman.new_keypair(&rand)
{
Ok(pair) => pair,
Err(_) => {
assert!(false, "Can't generate random keypair");
return;
}
};
let data = dirsync::ReqInner::ClearText(dirsync::ReqData {
nonce: dirsync::Nonce::new(&rand),
client_key_id: KeyID(2424),
id: ID::ID(::core::num::NonZeroU64::new(424242).unwrap()),
auth: dirsync::AuthInfo {
user: auth::UserID::new(&rand),
token: auth::Token::new_anonymous(&rand),
service_id: auth::SERVICEID_AUTH,
domain: auth::Domain("example.com".to_owned()),
},
});
let h_req = Handshake::new(HandshakeData::DirSync(dirsync::DirSync::Req(
dirsync::Req {
key_id: KeyID(4224),
exchange: enc::asym::KeyExchangeKind::X25519DiffieHellman,
hkdf: enc::hkdf::HkdfKind::Sha3,
cipher: enc::sym::CipherKind::XChaCha20Poly1305,
exchange_key,
data,
},
)));
let mut bytes = Vec::<u8>::with_capacity(h_req.len());
bytes.resize(h_req.len(), 0);
h_req.serialize(
cipher_send.kind().nonce_len(),
cipher_send.kind().tag_len(),
&mut bytes,
);
let deserialized = match Handshake::deserialize(&bytes) {
Ok(deserialized) => deserialized,
Err(e) => {
assert!(false, "{}", e.to_string());
return;
}
};
}

View File

@ -152,34 +152,56 @@ mod tests {
#[test] #[test]
fn test_serialization() { fn test_serialization() {
// The record was generated with: let rand = enc::Random::new();
// f-dnssec generate dnssec \ let (_, exchange_key) =
// -a 1 2 42 directory_synchronized 127.0.0.1 31337 \ match enc::asym::KeyExchangeKind::X25519DiffieHellman
// -p 42 x25519 x25519.pub \ .new_keypair(&rand)
// -x x25519diffiehellman \ {
// -c xchacha20poly1305 Ok(pair) => pair,
const TXT_RECORD: &'static str = "v=Fenrir1 \ Err(_) => {
5fBgo5ovk=0Dk}g0V)6>0cKP8KO-Vna846zp@MaLF|nim_XH&nQvT-I|B9HfJpcd"; assert!(false, "Can't generate random keypair");
return;
}
};
use crate::enc;
let record = Record {
public_keys : [(enc::asym::KeyID(42),
enc::asym::PubKey::Exchange(exchange_key))].to_vec(),
addresses: [record::Address {
ip: ::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127,0,0,1)),
port: Some(::core::num::NonZeroU16::new(31337).unwrap()),
priority: record::AddressPriority::P1,
weight: record::AddressWeight::W1,
handshake_ids: [crate::connection::handshake::HandshakeID::DirectorySynchronized].to_vec(),
public_key_idx : [record::PubKeyIdx(0)].to_vec(),
let record = match Dnssec::parse_txt_record(TXT_RECORD) { }].to_vec(),
key_exchanges: [enc::asym::KeyExchangeKind::X25519DiffieHellman].to_vec(),
hkdfs: [enc::hkdf::HkdfKind::Sha3].to_vec(),
ciphers: [enc::sym::CipherKind::XChaCha20Poly1305].to_vec(),
};
let encoded = match record.encode() {
Ok(encoded) => encoded,
Err(e) => {
assert!(false, "{}", e.to_string());
return;
}
};
let full_record = "v=Fenrir1 ".to_string() + &encoded;
let record = match Dnssec::parse_txt_record(&full_record) {
Ok(record) => record, Ok(record) => record,
Err(e) => { Err(e) => {
assert!(false, "{}", e.to_string()); assert!(false, "{}", e.to_string());
return; return;
} }
}; };
let re_encoded = match record.encode() { let _re_encoded = match record.encode() {
Ok(re_encoded) => re_encoded, Ok(re_encoded) => re_encoded,
Err(e) => { Err(e) => {
assert!(false, "{}", e.to_string()); assert!(false, "{}", e.to_string());
return; return;
} }
}; };
assert!(
TXT_RECORD[10..] == re_encoded,
"DNSSEC record decoding->encoding failed:\n{}\n{}",
TXT_RECORD,
re_encoded
);
} }
} }

View File

@ -429,7 +429,7 @@ impl Record {
+ self + self
.public_keys .public_keys
.iter() .iter()
.map(|(_, key)| 4 + key.kind().pub_len()) .map(|(_, key)| 3 + key.kind().pub_len())
.sum::<usize>() .sum::<usize>()
+ self.key_exchanges.len() + self.key_exchanges.len()
+ self.hkdfs.len() + self.hkdfs.len()
@ -463,7 +463,7 @@ impl Record {
let written_next = written + KeyID::len(); let written_next = written + KeyID::len();
raw[written..written_next].copy_from_slice(&key_id_bytes); raw[written..written_next].copy_from_slice(&key_id_bytes);
written = written_next; written = written_next;
raw[written] = public_key.kind().pub_len() as u8; raw[written] = public_key.len() as u8;
written = written + 1; written = written + 1;
let written_next = written + public_key.len(); let written_next = written + public_key.len();
public_key.serialize_into(&mut raw[written..written_next]); public_key.serialize_into(&mut raw[written..written_next]);
@ -531,10 +531,10 @@ impl Record {
let raw_key_id = let raw_key_id =
u16::from_le_bytes([raw[bytes_parsed], raw[bytes_parsed + 1]]); u16::from_le_bytes([raw[bytes_parsed], raw[bytes_parsed + 1]]);
let id = KeyID(raw_key_id); let id = KeyID(raw_key_id);
bytes_parsed = bytes_parsed + 2; bytes_parsed = bytes_parsed + KeyID::len();
let pubkey_length = raw[bytes_parsed] as usize; let pubkey_length = raw[bytes_parsed] as usize;
bytes_parsed = bytes_parsed + 1; bytes_parsed = bytes_parsed + 1;
let bytes_next_key = bytes_parsed + 1 + pubkey_length; let bytes_next_key = bytes_parsed + pubkey_length;
if bytes_next_key > raw.len() { if bytes_next_key > raw.len() {
return Err(Error::NotEnoughData(bytes_parsed)); return Err(Error::NotEnoughData(bytes_parsed));
} }
@ -551,7 +551,7 @@ impl Record {
return Err(Error::UnsupportedData(bytes_parsed)); return Err(Error::UnsupportedData(bytes_parsed));
} }
}; };
if bytes != 1 + pubkey_length { if bytes != pubkey_length {
return Err(Error::UnsupportedData(bytes_parsed)); return Err(Error::UnsupportedData(bytes_parsed));
} }
bytes_parsed = bytes_parsed + bytes; bytes_parsed = bytes_parsed + bytes;

View File

@ -93,16 +93,19 @@ pub enum KeyKind {
#[strum(serialize = "x25519")] #[strum(serialize = "x25519")]
X25519, X25519,
} }
// FIXME: actually check this
const MIN_KEY_SIZE: usize = 32;
impl KeyKind { impl KeyKind {
/// Length of the serialized field
pub const fn len() -> usize {
1
}
/// return the expected length of the public key /// return the expected length of the public key
pub fn pub_len(&self) -> usize { pub fn pub_len(&self) -> usize {
match self { KeyKind::len()
// FIXME: 99% wrong size + match self {
KeyKind::Ed25519 => ::ring::signature::ED25519_PUBLIC_KEY_LEN, // FIXME: 99% wrong size
KeyKind::X25519 => 32, KeyKind::Ed25519 => ::ring::signature::ED25519_PUBLIC_KEY_LEN,
} KeyKind::X25519 => 32,
}
} }
/// Get the capabilities of this key type /// Get the capabilities of this key type
pub fn capabilities(&self) -> KeyCapabilities { pub fn capabilities(&self) -> KeyCapabilities {
@ -185,7 +188,7 @@ pub enum PubKey {
impl PubKey { impl PubKey {
/// Get the serialized key length /// Get the serialized key length
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
1 + match self { match self {
PubKey::Exchange(ex) => ex.len(), PubKey::Exchange(ex) => ex.len(),
PubKey::Signing => todo!(), PubKey::Signing => todo!(),
} }
@ -215,17 +218,12 @@ impl PubKey {
/// serialize the key into the buffer /// serialize the key into the buffer
/// NOTE: Assumes there is enough space /// NOTE: Assumes there is enough space
pub fn serialize_into(&self, out: &mut [u8]) { pub fn serialize_into(&self, out: &mut [u8]) {
assert!(
out.len() >= 1 + self.kind().pub_len(),
"Not enough out buffer",
);
out[0] = self.kind() as u8;
match self { match self {
PubKey::Signing => { PubKey::Signing => {
::tracing::error!("serializing ed25519 not supported"); ::tracing::error!("serializing ed25519 not supported");
return; return;
} }
PubKey::Exchange(ex) => ex.serialize_into(&mut out[1..]), PubKey::Exchange(ex) => ex.serialize_into(out),
} }
} }
/// Try to deserialize the pubkey from raw bytes /// Try to deserialize the pubkey from raw bytes
@ -238,7 +236,7 @@ impl PubKey {
Some(kind) => kind, Some(kind) => kind,
None => return Err(Error::UnsupportedKey(1)), None => return Err(Error::UnsupportedKey(1)),
}; };
if raw.len() < 1 + kind.pub_len() { if raw.len() < kind.pub_len() {
return Err(Error::NotEnoughData(1)); return Err(Error::NotEnoughData(1));
} }
match kind { match kind {
@ -259,7 +257,7 @@ impl PubKey {
}; };
Ok(( Ok((
PubKey::Exchange(ExchangePubKey::X25519(pub_key)), PubKey::Exchange(ExchangePubKey::X25519(pub_key)),
1 + kind.pub_len(), kind.pub_len(),
)) ))
} }
} }
@ -281,7 +279,7 @@ pub enum PrivKey {
impl PrivKey { impl PrivKey {
/// Get the serialized key length /// Get the serialized key length
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
1 + match self { match self {
PrivKey::Exchange(ex) => ex.len(), PrivKey::Exchange(ex) => ex.len(),
PrivKey::Signing => todo!(), PrivKey::Signing => todo!(),
} }
@ -296,9 +294,8 @@ impl PrivKey {
/// serialize the key into the buffer /// serialize the key into the buffer
/// NOTE: Assumes there is enough space /// NOTE: Assumes there is enough space
pub fn serialize_into(&self, out: &mut [u8]) { pub fn serialize_into(&self, out: &mut [u8]) {
out[0] = self.kind() as u8;
match self { match self {
PrivKey::Exchange(ex) => ex.serialize_into(&mut out[1..]), PrivKey::Exchange(ex) => ex.serialize_into(out),
PrivKey::Signing => todo!(), PrivKey::Signing => todo!(),
} }
} }
@ -346,9 +343,10 @@ impl ExchangePrivKey {
/// serialize the key into the buffer /// serialize the key into the buffer
/// NOTE: Assumes there is enough space /// NOTE: Assumes there is enough space
pub fn serialize_into(&self, out: &mut [u8]) { pub fn serialize_into(&self, out: &mut [u8]) {
out[0] = self.kind() as u8;
match self { match self {
ExchangePrivKey::X25519(key) => { ExchangePrivKey::X25519(key) => {
out[0..32].copy_from_slice(&key.to_bytes()); out[1..33].copy_from_slice(&key.to_bytes());
} }
} }
} }
@ -378,21 +376,18 @@ impl ExchangePubKey {
/// serialize the key into the buffer /// serialize the key into the buffer
/// NOTE: Assumes there is enough space /// NOTE: Assumes there is enough space
pub fn serialize_into(&self, out: &mut [u8]) { pub fn serialize_into(&self, out: &mut [u8]) {
out[0] = self.kind() as u8;
match self { match self {
ExchangePubKey::X25519(pk) => { ExchangePubKey::X25519(pk) => {
let bytes = pk.as_bytes(); let bytes = pk.as_bytes();
assert!(bytes.len() == 32, "x25519 should have been 32 bytes"); out[1..33].copy_from_slice(bytes);
out[..32].copy_from_slice(bytes);
} }
} }
} }
/// Load public key used for key exchange from it raw bytes /// Load public key used for key exchange from it raw bytes
/// The riesult is "unparsed" since we don't verify /// The riesult is "unparsed" since we don't verify
/// the actual key /// the actual key
pub fn from_slice(raw: &[u8]) -> Result<(Self, usize), Error> { pub fn deserialize(raw: &[u8]) -> Result<(Self, usize), Error> {
if raw.len() < 1 + MIN_KEY_SIZE {
return Err(Error::NotEnoughData(0));
}
match KeyKind::from_u8(raw[0]) { match KeyKind::from_u8(raw[0]) {
Some(kind) => match kind { Some(kind) => match kind {
KeyKind::Ed25519 => { KeyKind::Ed25519 => {