use std::borrow::Cow;
use ecdsa::signature::{
self,
hazmat::{PrehashSigner, PrehashVerifier},
};
use prost::{DecodeError, Message};
use prost_types::Timestamp;
use scion_protobuf::control_plane::v1::VerificationKeyId;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum DigestAlgorithm {
Sha256,
Sha384,
Sha512,
}
#[derive(Debug, Error)]
pub enum ValidateError {
#[error("invalid header and body")]
InvalidHeaderAndBody,
#[error("invalid header")]
InvalidHeader,
#[error("invalid validation key id")]
InvalidValidationKeyId,
#[error("associated data length mismatch (expected {expected}, got {actual})")]
InvalidAssociatedDataLength {
expected: usize,
actual: usize,
},
#[error("invalid digest algorithm")]
InvalidDigestAlgorithm,
#[error("invalid body")]
InvalidBody,
#[error("invalid metadata")]
InvalidMetadata,
#[error("no key matching key found: {0}")]
KeyMissing(Cow<'static, str>),
#[error("signature malformed")]
SignatureMalformed,
#[error("signature verification failed")]
SignatureVerificationFailed(#[source] ecdsa::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SignedMessage {
pub header_and_body: Vec<u8>,
pub signature: Vec<u8>,
}
impl SignedMessage {
pub fn sign<'a>(
key: &'a p256::ecdsa::SigningKey,
digest_algo: DigestAlgorithm,
timestamp: u32,
verification_key_id: Option<VerificationKeyId>,
associated_data: (usize, impl IntoIterator<Item = &'a [u8]>),
message: &impl prost::Message,
metadata: &impl prost::Message,
) -> signature::Result<SignedMessage> {
let signature_algorithm = match digest_algo {
DigestAlgorithm::Sha256 => {
scion_protobuf::crypto::v1::SignatureAlgorithm::EcdsaWithSha256
}
DigestAlgorithm::Sha384 => {
scion_protobuf::crypto::v1::SignatureAlgorithm::EcdsaWithSha384
}
DigestAlgorithm::Sha512 => {
scion_protobuf::crypto::v1::SignatureAlgorithm::EcdsaWithSha512
}
};
let verification_key_id = verification_key_id.map(|k| k.encode_to_vec());
let header = scion_protobuf::crypto::v1::Header {
signature_algorithm: signature_algorithm as i32,
verification_key_id: verification_key_id.unwrap_or_default(),
timestamp: Some(Timestamp {
seconds: timestamp as i64,
nanos: 0,
}),
associated_data_length: associated_data.0 as i32,
metadata: metadata.encode_to_vec(),
};
let header_and_body = scion_protobuf::crypto::v1::HeaderAndBodyInternal {
header: header.encode_to_vec(),
body: message.encode_to_vec(),
};
let encoded_header_and_body = header_and_body.encode_to_vec();
let signature: p256::ecdsa::Signature = {
let hash = match digest_algo {
DigestAlgorithm::Sha256 => {
hash::<sha2::Sha256>(&encoded_header_and_body, associated_data.1)
}
DigestAlgorithm::Sha384 => {
hash::<sha2::Sha384>(&encoded_header_and_body, associated_data.1)
}
DigestAlgorithm::Sha512 => {
hash::<sha2::Sha512>(&encoded_header_and_body, associated_data.1)
}
};
key.sign_prehash(&hash)?
};
let signature = signature.to_der().as_bytes().to_vec();
Ok(SignedMessage {
header_and_body: encoded_header_and_body,
signature,
})
}
}
impl SignedMessage {
pub fn validate<'a>(
&'a self,
key_provider: impl Fn(&[u8]) -> Result<p256::ecdsa::VerifyingKey, ValidateError>,
associated_data: (usize, impl IntoIterator<Item = &'a [u8]>),
) -> Result<(scion_protobuf::crypto::v1::Header, Vec<u8>), ValidateError> {
{
let header_and_body = scion_protobuf::crypto::v1::HeaderAndBodyInternal::decode(
&self.header_and_body[..],
)
.map_err(|_| ValidateError::InvalidHeaderAndBody)?;
let header = scion_protobuf::crypto::v1::Header::decode(&header_and_body.header[..])
.map_err(|_| ValidateError::InvalidHeader)?;
let verification_key = key_provider(&header.verification_key_id[..])?;
if header.associated_data_length as usize != associated_data.0 {
return Err(ValidateError::InvalidAssociatedDataLength {
expected: header.associated_data_length as usize,
actual: associated_data.0,
});
}
let algo = match scion_protobuf::crypto::v1::SignatureAlgorithm::try_from(
header.signature_algorithm,
)
.ok()
{
Some(scion_protobuf::crypto::v1::SignatureAlgorithm::EcdsaWithSha256) => {
DigestAlgorithm::Sha256
}
Some(scion_protobuf::crypto::v1::SignatureAlgorithm::EcdsaWithSha384) => {
DigestAlgorithm::Sha384
}
Some(scion_protobuf::crypto::v1::SignatureAlgorithm::EcdsaWithSha512) => {
DigestAlgorithm::Sha512
}
_ => return Err(ValidateError::InvalidDigestAlgorithm),
};
let hash = {
match algo {
DigestAlgorithm::Sha256 => {
hash::<sha2::Sha256>(self.header_and_body.as_slice(), associated_data.1)
}
DigestAlgorithm::Sha384 => {
hash::<sha2::Sha384>(self.header_and_body.as_slice(), associated_data.1)
}
DigestAlgorithm::Sha512 => {
hash::<sha2::Sha512>(self.header_and_body.as_slice(), associated_data.1)
}
}
};
let sig = p256::ecdsa::Signature::from_der(&self.signature)
.map_err(|_| ValidateError::SignatureMalformed)?;
verification_key
.verify_prehash(&hash, &sig)
.map_err(ValidateError::SignatureVerificationFailed)?;
Ok((header, header_and_body.body))
}
}
}
impl SignedMessage {
#[inline]
pub fn decode_validated<'a, Body, Metadata>(
&'a self,
key_provider: impl Fn(&[u8]) -> Result<p256::ecdsa::VerifyingKey, ValidateError>,
associated_data: (usize, impl IntoIterator<Item = &'a [u8]>),
) -> Result<(Body, Option<Metadata>), ValidateError>
where
Body: prost::Message + Default,
Metadata: prost::Message + Default,
{
let (header, body) = self.validate(key_provider, associated_data)?;
let body = Body::decode(&body[..]).map_err(|_| ValidateError::InvalidBody)?;
let metadata = if !header.metadata.is_empty() {
Some(
Metadata::decode(&header.metadata[..])
.map_err(|_| ValidateError::InvalidMetadata)?,
)
} else {
None
};
Ok((body, metadata))
}
#[inline]
pub fn decode_unvalidated<Body, Metadata>(
&self,
) -> Result<(Body, Option<Metadata>), DecodeError>
where
Body: prost::Message + Default,
Metadata: prost::Message + Default,
{
let header_and_body =
scion_protobuf::crypto::v1::HeaderAndBodyInternal::decode(&self.header_and_body[..])?;
let header = scion_protobuf::crypto::v1::Header::decode(&header_and_body.header[..])?;
let body = Body::decode(&header_and_body.body[..])?;
let metadata = if !header.metadata.is_empty() {
Some(Metadata::decode(&header.metadata[..])?)
} else {
None
};
Ok((body, metadata))
}
}
impl SignedMessage {
#[inline]
pub fn into_rpc(self) -> scion_protobuf::crypto::v1::SignedMessage {
scion_protobuf::crypto::v1::SignedMessage {
header_and_body: self.header_and_body,
signature: self.signature,
}
}
#[inline]
pub fn from_rpc(value: scion_protobuf::crypto::v1::SignedMessage) -> Self {
Self {
header_and_body: value.header_and_body,
signature: value.signature,
}
}
}
impl From<scion_protobuf::crypto::v1::SignedMessage> for SignedMessage {
#[inline]
fn from(value: scion_protobuf::crypto::v1::SignedMessage) -> Self {
SignedMessage::from_rpc(value)
}
}
impl From<SignedMessage> for scion_protobuf::crypto::v1::SignedMessage {
#[inline]
fn from(signed: SignedMessage) -> Self {
signed.into_rpc()
}
}
#[inline]
fn hash<D: Digest>(msg: &[u8], data: impl IntoIterator<Item: AsRef<[u8]>>) -> Vec<u8> {
let mut hasher = D::new();
hasher.update(msg);
for chunk in data {
hasher.update(chunk.as_ref());
}
hasher.finalize().to_vec()
}
#[cfg(test)]
mod test {
use ecdsa::signature::rand_core::OsRng;
use prost::Message;
use super::*;
fn test_data() -> (
p256::ecdsa::SigningKey,
VerificationKeyId,
Vec<u8>,
scion_protobuf::control_plane::v1::SegmentsRequest,
scion_protobuf::control_plane::v1::SegmentsRequest,
) {
let key = p256::ecdsa::SigningKey::random(&mut OsRng);
let key_id: Vec<u8> = key.to_bytes()[..16].to_vec();
let key_id = VerificationKeyId {
isd_as: 1,
subject_key_id: key_id.clone(),
trc_base: 125,
trc_serial: 31536,
};
let assoc_data = "hello crypto".as_bytes().to_vec();
let message = scion_protobuf::control_plane::v1::SegmentsRequest {
src_isd_as: 1,
dst_isd_as: 2,
};
let metadata = scion_protobuf::control_plane::v1::SegmentsRequest {
src_isd_as: 2,
dst_isd_as: 3,
};
(key, key_id, assoc_data, message, metadata)
}
fn signed_message_with_metadata(
key: &p256::ecdsa::SigningKey,
key_id: VerificationKeyId,
assoc_data: &Vec<u8>,
message: &scion_protobuf::control_plane::v1::SegmentsRequest,
metadata: &scion_protobuf::control_plane::v1::SegmentsRequest,
) -> SignedMessage {
SignedMessage::sign(
key,
DigestAlgorithm::Sha256,
0, Some(key_id),
(assoc_data.len(), std::iter::once(assoc_data.as_slice())),
message,
metadata,
)
.expect("failed to sign message")
}
#[test]
fn should_roundtrip() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id.clone(), &assoc_data, &message, &metadata);
let verifying_key = key.verifying_key();
let validation_result = signed_message
.decode_validated::<scion_protobuf::control_plane::v1::SegmentsRequest, scion_protobuf::control_plane::v1::SegmentsRequest>( |encoded_key_id| {
let decoded_key_id = VerificationKeyId::decode(encoded_key_id)
.map_err(|_| ValidateError::InvalidValidationKeyId)?;
if decoded_key_id != key_id {
return Err(ValidateError::KeyMissing(format!(
"expected key id {:?}, got {:?}",
key_id, decoded_key_id
).into()));
}
Ok(*verifying_key)
},
(assoc_data.len(), std::iter::once(assoc_data.as_slice())),
);
assert!(
validation_result.is_ok(),
"signature validation failed: {:?}",
validation_result
);
let (decoded_message, decoded_metadata) = validation_result.unwrap();
assert_eq!(
decoded_message, message,
"decoded message does not match original"
);
assert_eq!(
decoded_metadata.unwrap(),
metadata,
"decoded metadata does not match original"
);
}
#[test]
fn should_fail_on_tampered_body() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id.clone(), &assoc_data, &message, &metadata);
let mut tampered = signed_message.clone();
let mut header_and_body = scion_protobuf::crypto::v1::HeaderAndBodyInternal::decode(
&tampered.header_and_body[..],
)
.expect("failed to decode header and body");
if let Some(byte) = header_and_body.body.first_mut() {
*byte ^= 0x01;
} else {
header_and_body.body.push(0x01);
}
tampered.header_and_body = header_and_body.encode_to_vec();
let verifying_key = key.verifying_key();
let result = tampered.validate(
|encoded_key_id| {
let decoded_key_id = VerificationKeyId::decode(encoded_key_id)
.map_err(|_| ValidateError::InvalidValidationKeyId)?;
if decoded_key_id != key_id {
return Err(ValidateError::KeyMissing(
format!("expected key id {:?}, got {:?}", key_id, decoded_key_id).into(),
));
}
Ok(*verifying_key)
},
(assoc_data.len(), std::iter::once(assoc_data.as_slice())),
);
assert!(matches!(
result,
Err(ValidateError::SignatureVerificationFailed(_))
));
}
#[test]
fn should_fail_on_wrong_key_id() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id.clone(), &assoc_data, &message, &metadata);
let verifying_key = key.verifying_key();
let wrong_key_id_bytes: Vec<u8> = key_id.subject_key_id.iter().map(|b| b ^ 0x01).collect();
let result = signed_message.validate(
|encoded_key_id| {
if encoded_key_id != wrong_key_id_bytes {
return Err(ValidateError::KeyMissing("key id mismatch".into()));
}
Ok(*verifying_key)
},
(assoc_data.len(), std::iter::once(assoc_data.as_slice())),
);
assert!(matches!(result, Err(ValidateError::KeyMissing(_))));
}
#[test]
fn should_fail_on_wrong_associated_data_len() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id.clone(), &assoc_data, &message, &metadata);
let verifying_key = key.verifying_key();
let wrong_assoc_data = "wrong data".as_bytes().to_vec();
let result = signed_message.validate(
|encoded_key_id| {
let decoded_key_id = VerificationKeyId::decode(encoded_key_id)
.map_err(|_| ValidateError::InvalidValidationKeyId)?;
if decoded_key_id != key_id {
return Err(ValidateError::KeyMissing(
format!("expected key id {:?}, got {:?}", key_id, decoded_key_id).into(),
));
}
Ok(*verifying_key)
},
(
wrong_assoc_data.len(),
std::iter::once(wrong_assoc_data.as_slice()),
),
);
assert!(
matches!(
result,
Err(ValidateError::InvalidAssociatedDataLength { .. })
),
"validation should fail with InvalidAssociatedDataLength, but got: {result:?}"
);
}
#[test]
fn should_fail_on_wrong_associated_data() {
let (key, key_id, mut assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id.clone(), &assoc_data, &message, &metadata);
assoc_data[0] ^= 0x01;
let verifying_key = key.verifying_key();
let result = signed_message.validate(
|encoded_key_id| {
let decoded_key_id = VerificationKeyId::decode(encoded_key_id)
.map_err(|_| ValidateError::InvalidValidationKeyId)?;
if decoded_key_id != key_id {
return Err(ValidateError::KeyMissing(
format!("expected key id {:?}, got {:?}", key_id, decoded_key_id).into(),
));
}
Ok(*verifying_key)
},
(assoc_data.len(), std::iter::once(assoc_data.as_slice())),
);
assert!(
matches!(result, Err(ValidateError::SignatureVerificationFailed(_))),
"validation should fail with SignatureVerificationFailed, but got: {result:?}"
);
}
#[test]
fn should_fail_on_wrong_body_type() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id, &assoc_data, &message, &metadata);
let mut header_and_body = scion_protobuf::crypto::v1::HeaderAndBodyInternal::decode(
&signed_message.header_and_body[..],
)
.expect("failed to decode header and body");
header_and_body.body = vec![0xff];
let invalid_body_message = SignedMessage {
header_and_body: header_and_body.encode_to_vec(),
signature: Vec::new(),
};
let decode_result = invalid_body_message.decode_unvalidated::<
scion_protobuf::control_plane::v1::SegmentsRequest,
scion_protobuf::control_plane::v1::SegmentsRequest,
>();
assert!(decode_result.is_err());
}
#[test]
fn should_fail_on_wrong_metadata_type() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id, &assoc_data, &message, &metadata);
let mut header_and_body = scion_protobuf::crypto::v1::HeaderAndBodyInternal::decode(
&signed_message.header_and_body[..],
)
.expect("failed to decode header and body");
let mut header = scion_protobuf::crypto::v1::Header::decode(&header_and_body.header[..])
.expect("failed to decode header");
header.metadata = vec![0xff];
header_and_body.header = header.encode_to_vec();
let invalid_metadata_message = SignedMessage {
header_and_body: header_and_body.encode_to_vec(),
signature: Vec::new(),
};
let decode_result = invalid_metadata_message.decode_unvalidated::<
scion_protobuf::control_plane::v1::SegmentsRequest,
scion_protobuf::control_plane::v1::SegmentsRequest,
>();
assert!(decode_result.is_err());
}
#[test]
fn should_ignore_metadata_with_empty_message_type() {
let (key, key_id, assoc_data, message, metadata) = test_data();
let signed_message =
signed_message_with_metadata(&key, key_id.clone(), &assoc_data, &message, &metadata);
let verifying_key = key.verifying_key();
let result = signed_message
.decode_validated::<scion_protobuf::control_plane::v1::SegmentsRequest, ()>(
|encoded_key_id| {
let decoded_key_id = VerificationKeyId::decode(encoded_key_id)
.map_err(|_| ValidateError::InvalidValidationKeyId)?;
if decoded_key_id != key_id {
return Err(ValidateError::KeyMissing(
format!("expected key id {:?}, got {:?}", key_id, decoded_key_id)
.into(),
));
}
Ok(*verifying_key)
},
(assoc_data.len(), std::iter::once(assoc_data.as_slice())),
);
assert!(result.is_ok(), "validation failed: {result:?}");
let (decoded_message, decoded_metadata) = result.unwrap();
assert_eq!(decoded_message, message);
assert!(decoded_metadata.is_some());
}
}