use bottlers::PrivateKey;
use ciborium::value::Value;
use crate::error::Error;
#[derive(Debug, Clone, Default)]
pub struct HandshakeRequest {
pub ready: bool,
pub server_code: String,
pub client_id: String,
pub nonce: Vec<u8>,
pub groups: Option<Vec<Vec<u8>>>,
raw: Option<Vec<u8>>,
}
impl HandshakeRequest {
pub fn to_cbor(&self) -> Result<Vec<u8>, Error> {
let mut map: Vec<(Value, Value)> = Vec::new();
if self.ready {
map.push((Value::Text("rdy".into()), Value::Bool(true)));
}
map.push((
Value::Text("srv".into()),
Value::Text(self.server_code.clone()),
));
map.push((
Value::Text("cid".into()),
Value::Text(self.client_id.clone()),
));
map.push((Value::Text("rnd".into()), Value::Bytes(self.nonce.clone())));
map.push((
Value::Text("grp".into()),
match &self.groups {
None => Value::Null,
Some(g) => Value::Array(g.iter().map(|b| Value::Bytes(b.clone())).collect()),
},
));
to_cbor(&Value::Map(map))
}
pub fn from_cbor(buf: &[u8]) -> Result<Self, Error> {
let v: Value = ciborium::from_reader(buf).map_err(|e| Error::Cbor(e.to_string()))?;
let Value::Map(map) = v else {
return Err(Error::Cbor("handshake request must be a map".into()));
};
let mut req = HandshakeRequest {
raw: Some(buf.to_vec()),
..Default::default()
};
for (k, v) in map {
let Value::Text(key) = k else { continue };
match (key.as_str(), v) {
("rdy", Value::Bool(b)) => req.ready = b,
("srv", Value::Text(s)) => req.server_code = s,
("cid", Value::Text(s)) => req.client_id = s,
("rnd", Value::Bytes(b)) => req.nonce = b,
("grp", Value::Array(items)) => {
let mut groups = Vec::with_capacity(items.len());
for item in items {
if let Value::Bytes(b) = item {
groups.push(b);
}
}
req.groups = Some(groups);
}
_ => {}
}
}
Ok(req)
}
pub fn respond(&self, key: &PrivateKey) -> Result<HandshakeResponse, Error> {
let raw = match &self.raw {
Some(r) => r.clone(),
None => self.to_cbor()?,
};
let sig = bottlers::sign::sign(key, &raw)?;
Ok(HandshakeResponse {
id: Vec::new(),
key: key.public_pkix()?,
sig,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct HandshakeResponse {
pub id: Vec<u8>,
pub key: Vec<u8>,
pub sig: Vec<u8>,
}
impl HandshakeResponse {
pub fn to_cbor(&self) -> Result<Vec<u8>, Error> {
let map: Vec<(Value, Value)> = vec![
(
Value::Text("id".into()),
if self.id.is_empty() {
Value::Null
} else {
Value::Bytes(self.id.clone())
},
),
(Value::Text("key".into()), Value::Bytes(self.key.clone())),
(Value::Text("sig".into()), Value::Bytes(self.sig.clone())),
];
to_cbor(&Value::Map(map))
}
pub fn from_cbor(buf: &[u8]) -> Result<Self, Error> {
let v: Value = ciborium::from_reader(buf).map_err(|e| Error::Cbor(e.to_string()))?;
let Value::Map(map) = v else {
return Err(Error::Cbor("handshake response must be a map".into()));
};
let mut res = HandshakeResponse::default();
for (k, v) in map {
let Value::Text(key) = k else { continue };
match (key.as_str(), v) {
("id", Value::Bytes(b)) => res.id = b,
("key", Value::Bytes(b)) => res.key = b,
("sig", Value::Bytes(b)) => res.sig = b,
_ => {}
}
}
Ok(res)
}
}
fn to_cbor(v: &Value) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
ciborium::into_writer(v, &mut out).map_err(|e| Error::Cbor(e.to_string()))?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trip() {
let req = HandshakeRequest {
ready: false,
server_code: "srv1".into(),
client_id: "srv1.conn-abc".into(),
nonce: vec![1, 2, 3, 4],
groups: None,
raw: None,
};
let buf = req.to_cbor().unwrap();
let back = HandshakeRequest::from_cbor(&buf).unwrap();
assert_eq!(back.server_code, "srv1");
assert_eq!(back.client_id, "srv1.conn-abc");
assert_eq!(back.nonce, vec![1, 2, 3, 4]);
assert!(!back.ready);
assert!(back.groups.is_none());
}
#[test]
fn respond_signs_raw_buffer() {
use purecrypto::rng::OsRng;
let sk = purecrypto::ec::ecdsa::EcdsaPrivateKey::generate(&mut OsRng);
let key = PrivateKey::Ecdsa(sk);
let req = HandshakeRequest {
server_code: "s".into(),
client_id: "s.c".into(),
nonce: vec![9; 32],
..Default::default()
};
let buf = req.to_cbor().unwrap();
let parsed = HandshakeRequest::from_cbor(&buf).unwrap();
let res = parsed.respond(&key).unwrap();
assert!(!res.key.is_empty());
bottlers::sign::verify_pkix(&res.key, &buf, &res.sig).unwrap();
let enc = res.to_cbor().unwrap();
let back = HandshakeResponse::from_cbor(&enc).unwrap();
assert_eq!(back.key, res.key);
assert_eq!(back.sig, res.sig);
assert!(back.id.is_empty());
}
}