use ed25519_dalek::VerifyingKey;
use lys_core::keys::Ed25519Identity;
pub const LINK_NOISE_PATTERN: &str = "Noise_KK_25519_ChaChaPoly_BLAKE2s";
pub const NOISE_OVERHEAD: usize = 96;
pub const NOISE_MAX_MESSAGE: usize = 65_535;
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct MachineIdentity {
ed25519_public: [u8; 32],
}
impl std::fmt::Debug for MachineIdentity {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("MachineIdentity")
.field("ed25519_public", &"32 bytes")
.finish()
}
}
impl MachineIdentity {
#[must_use]
pub const fn new(ed25519_public: [u8; 32]) -> Self {
Self { ed25519_public }
}
#[must_use]
pub const fn ed25519_public(&self) -> &[u8; 32] {
&self.ed25519_public
}
pub fn x25519_public(&self) -> Result<[u8; 32], LinkHandshakeError> {
VerifyingKey::from_bytes(&self.ed25519_public)
.map(|key| key.to_montgomery().to_bytes())
.map_err(|_| LinkHandshakeError::InvalidIdentity)
}
}
pub struct LocalMachine {
identity: Ed25519Identity,
}
impl std::fmt::Debug for LocalMachine {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LocalMachine")
.field("identity", &self.machine())
.finish()
}
}
impl LocalMachine {
#[must_use]
pub const fn new(identity: Ed25519Identity) -> Self {
Self { identity }
}
#[must_use]
pub fn machine(&self) -> MachineIdentity {
MachineIdentity::new(self.identity.public_key_bytes())
}
fn x25519_secret(&self) -> [u8; 32] {
self.identity.x25519_static_secret().to_bytes()
}
pub(crate) fn certificate_proof(
&self,
agent: &str,
path: &str,
body: &[u8],
) -> Result<String, crate::link::certificate::CertificateRefusal> {
use crate::link::certificate::CertificateRefusal;
use std::io::Read;
let mut nonce = [0; 16];
std::fs::File::open("/dev/urandom")
.and_then(|mut file| file.read_exact(&mut nonce))
.map_err(|_| CertificateRefusal::StatusUnavailable)?;
let at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| CertificateRefusal::StatusUnavailable)?;
let at =
u64::try_from(at.as_millis()).map_err(|_| CertificateRefusal::StatusUnavailable)?;
Ok(crate::link_pass::cose::header(
&self.identity,
agent,
path,
body,
at,
&hex::encode(nonce),
))
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum LinkHandshakeError {
InvalidIdentity,
PeerNotTheExpectedMachine,
Framing(String),
Setup(String),
NotFinished,
}
impl std::fmt::Display for LinkHandshakeError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidIdentity => formatter.write_str("machine identity is not an Ed25519 key"),
Self::PeerNotTheExpectedMachine => {
formatter.write_str("peer is not the machine this link was opened with")
}
Self::Framing(detail) => write!(formatter, "noise framing refused: {detail}"),
Self::Setup(detail) => write!(formatter, "noise setup refused: {detail}"),
Self::NotFinished => formatter.write_str("handshake has not finished"),
}
}
}
impl std::error::Error for LinkHandshakeError {}
fn classify(error: &snow::Error) -> LinkHandshakeError {
match error {
snow::Error::Decrypt => LinkHandshakeError::PeerNotTheExpectedMachine,
snow::Error::Input => LinkHandshakeError::Framing("input outside bounds".to_owned()),
other => LinkHandshakeError::Framing(other.to_string()),
}
}
pub struct LinkHandshake {
state: snow::HandshakeState,
peer: MachineIdentity,
}
impl std::fmt::Debug for LinkHandshake {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LinkHandshake")
.field("peer", &self.peer)
.field("finished", &self.state.is_handshake_finished())
.finish()
}
}
impl LinkHandshake {
fn build(
local: &LocalMachine,
peer: MachineIdentity,
initiator: bool,
) -> Result<Self, LinkHandshakeError> {
let params: snow::params::NoiseParams = LINK_NOISE_PATTERN
.parse()
.map_err(|error: snow::Error| LinkHandshakeError::Setup(error.to_string()))?;
let secret = local.x25519_secret();
let remote = peer.x25519_public()?;
let builder = snow::Builder::new(params)
.local_private_key(&secret)
.map_err(|error| LinkHandshakeError::Setup(error.to_string()))?
.remote_public_key(&remote)
.map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
let state = if initiator {
builder.build_initiator()
} else {
builder.build_responder()
}
.map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
Ok(Self { state, peer })
}
pub fn initiator(
local: &LocalMachine,
peer: MachineIdentity,
) -> Result<Self, LinkHandshakeError> {
Self::build(local, peer, true)
}
pub fn responder(
local: &LocalMachine,
peer: MachineIdentity,
) -> Result<Self, LinkHandshakeError> {
Self::build(local, peer, false)
}
pub fn write(&mut self, out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
self.state
.write_message(&[], out)
.map_err(|error| classify(&error))
}
pub fn read(&mut self, message: &[u8]) -> Result<(), LinkHandshakeError> {
let mut payload = [0_u8; NOISE_OVERHEAD];
self.state
.read_message(message, &mut payload)
.map(|_| ())
.map_err(|error| classify(&error))
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.state.is_handshake_finished()
}
pub fn finish(self) -> Result<LinkTransport, LinkHandshakeError> {
if !self.state.is_handshake_finished() {
return Err(LinkHandshakeError::NotFinished);
}
let transport = self
.state
.into_transport_mode()
.map_err(|error| LinkHandshakeError::Setup(error.to_string()))?;
Ok(LinkTransport {
transport,
peer: self.peer,
})
}
}
pub struct LinkTransport {
transport: snow::TransportState,
peer: MachineIdentity,
}
impl std::fmt::Debug for LinkTransport {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LinkTransport")
.field("peer", &self.peer)
.finish_non_exhaustive()
}
}
impl LinkTransport {
#[must_use]
pub fn sending_nonce(&self) -> u64 {
self.transport.sending_nonce()
}
pub fn set_receiving_nonce(&mut self, nonce: u64) {
self.transport.set_receiving_nonce(nonce);
}
#[must_use]
pub const fn peer(&self) -> &MachineIdentity {
&self.peer
}
pub fn seal(&mut self, payload: &[u8], out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
self.transport
.write_message(payload, out)
.map_err(|error| classify(&error))
}
pub fn open(&mut self, frame: &[u8], out: &mut [u8]) -> Result<usize, LinkHandshakeError> {
self.transport
.read_message(frame, out)
.map_err(|error| classify(&error))
}
}