use ed25519_dalek::{Signature, SigningKey, VerifyingKey, Signer, Verifier};
use prost::Message;
use thiserror::Error;
use tonic::{Request, Response, Status, service::Interceptor};
use tonic::metadata::{MetadataKey, BinaryMetadataValue, Binary};
use std::convert::TryFrom;
#[derive(Error, Debug)]
pub enum Ed25519ClientError {
#[error("Protobuf encoding error: {0}")]
ProstEncode(#[from] prost::EncodeError),
#[error("Signature parsing error: {0}")]
SignatureError(#[from] ed25519_dalek::SignatureError),
#[error("gRPC status error: {0}")]
TonicStatus(#[from] Status),
}
pub fn sign_bytes(key: &SigningKey, data: &[u8]) -> Signature {
key.sign(data)
}
pub fn verify_bytes(
key: &VerifyingKey,
data: &[u8],
signature: &Signature,
) -> Result<(), ed25519_dalek::SignatureError> {
key.verify(data, signature)
}
pub fn sign_message<M: Message>(
key: &SigningKey,
message: &M,
) -> Result<Signature, prost::EncodeError> {
let mut buf = Vec::with_capacity(message.encoded_len());
message.encode(&mut buf)?;
Ok(sign_bytes(key, &buf))
}
pub fn verify_message<M: Message>(
key: &VerifyingKey,
message: &M,
signature: &Signature,
) -> Result<(), ed25519_dalek::SignatureError> {
let mut buf = Vec::with_capacity(message.encoded_len());
message.encode(&mut buf).expect("Encoding failed");
verify_bytes(key, &buf, signature)
}
pub struct SigningInterceptor {
signing_key: SigningKey,
sig_header: MetadataKey<Binary>,
pubkey_header: MetadataKey<Binary>,
}
impl SigningInterceptor {
pub fn new(
signing_key: SigningKey,
sig_header: impl Into<MetadataKey<Binary>>,
pubkey_header: impl Into<MetadataKey<Binary>>,
) -> Self {
Self {
signing_key,
sig_header: sig_header.into(),
pubkey_header: pubkey_header.into(),
}
}
}
impl Interceptor for SigningInterceptor {
fn call(&mut self, mut req: Request<()>) -> Result<Request<()>, Status> {
let mut buf = Vec::new();
req.get_ref()
.encode(&mut buf)
.map_err(|e| Status::internal(format!("Encode error: {}", e)))?;
let sig_bytes = self.signing_key.sign(&buf).to_bytes();
let pubkey_bytes = self.signing_key.verifying_key().to_bytes();
req.metadata_mut().insert_bin(
self.sig_header.clone(),
BinaryMetadataValue::from_bytes(&sig_bytes),
);
req.metadata_mut().insert_bin(
self.pubkey_header.clone(),
BinaryMetadataValue::from_bytes(&pubkey_bytes),
);
Ok(req)
}
}
pub struct ResponseVerifier {
server_pubkey: VerifyingKey,
sig_header: MetadataKey<Binary>,
}
impl ResponseVerifier {
pub fn new(
server_pubkey: VerifyingKey,
sig_header: impl Into<MetadataKey<Binary>>,
) -> Self {
Self { server_pubkey, sig_header: sig_header.into() }
}
pub fn verify<M: Message>(
&self,
response: &Response<M>,
) -> Result<(), Ed25519ClientError> {
let bytes = response
.metadata()
.get_bin(&self.sig_header)
.ok_or_else(|| Status::unauthenticated("Missing signature header"))?;
let signature = Signature::try_from(bytes.as_ref())
.map_err(|_| Status::unauthenticated("Invalid signature"))?;
let mut buf = Vec::with_capacity(response.get_ref().encoded_len());
response.get_ref()
.encode(&mut buf)
.map_err(Ed25519ClientError::ProstEncode)?;
self.server_pubkey
.verify(&buf, &signature)
.map_err(Ed25519ClientError::SignatureError)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use prost::Message;
use tonic::Response;
use rand::rngs::OsRng;
#[derive(Message, Clone)]
struct TestMsg {
#[prost(string, tag = "1")]
pub field: String,
}
#[tokio::test]
async fn test_sign_and_verify_functions() {
let mut rng = OsRng;
let key = SigningKey::generate(&mut rng);
let msg = TestMsg { field: "hello".into() };
let sig = sign_message(&key, &msg).expect("sign message");
verify_message(&key.verifying_key(), &msg, &sig).expect("verify message");
}
#[tokio::test]
async fn test_interceptor_and_verifier() {
let mut rng = OsRng;
let client_key = SigningKey::generate(&mut rng);
let server_key = SigningKey::generate(&mut rng);
let msg = TestMsg { field: "data".into() };
let mut response = Response::new(msg.clone());
let sig = server_key.sign(&msg.field.as_bytes()).to_bytes().to_vec();
response.metadata_mut().insert_bin("x-signature-bin".parse().unwrap(), sig);
let verifier = ResponseVerifier::new(server_key.verifying_key(), "x-signature-bin");
verifier.verify(&response).expect("verify response");
}
}