Skip to main content

kcode_k1_http_signature/
lib.rs

1use ed25519_dalek::{Signature, VerifyingKey};
2use std::error::Error;
3use std::fmt::{self, Display, Formatter};
4
5const MAGIC: &[u8; 9] = b"K1-HTTP-1";
6const FIXED_ENCODED_LENGTH: usize = MAGIC.len() + 6 * 8 + 8 + 16 + 32;
7
8#[derive(Clone, Debug, Eq, Hash, PartialEq)]
9pub struct CanonicalUsername(String);
10
11impl CanonicalUsername {
12    pub fn parse(value: &str) -> Result<Self, UsernameError> {
13        if value.is_empty() {
14            return Err(UsernameError::Empty);
15        }
16        if value.len() > 64 {
17            return Err(UsernameError::TooLong);
18        }
19        if !value.is_ascii() {
20            return Err(UsernameError::NonAscii);
21        }
22        if value.bytes().any(|byte| byte.is_ascii_control()) {
23            return Err(UsernameError::ControlCharacter);
24        }
25        Ok(Self(value.to_ascii_lowercase()))
26    }
27
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum UsernameError {
35    Empty,
36    TooLong,
37    NonAscii,
38    ControlCharacter,
39}
40
41impl Display for UsernameError {
42    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
43        let message = match self {
44            Self::Empty => "username must contain at least one byte",
45            Self::TooLong => "username must contain at most 64 bytes",
46            Self::NonAscii => "username must contain only ASCII bytes",
47            Self::ControlCharacter => "username must not contain ASCII control characters",
48        };
49        formatter.write_str(message)
50    }
51}
52
53impl Error for UsernameError {}
54
55pub struct RequestBinding<'a> {
56    pub server_id: &'a str,
57    pub public_origin: &'a str,
58    pub username: &'a CanonicalUsername,
59    pub epoch: u64,
60    pub nonce: [u8; 16],
61    pub method: &'a str,
62    pub target: &'a str,
63    pub content_type: &'a str,
64    pub body_sha256: [u8; 32],
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum SignatureError {
69    MalformedPublicKey,
70    VerificationFailed,
71}
72
73impl Display for SignatureError {
74    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
75        let message = match self {
76            Self::MalformedPublicKey => "malformed Ed25519 public key",
77            Self::VerificationFailed => "Ed25519 signature verification failed",
78        };
79        formatter.write_str(message)
80    }
81}
82
83impl Error for SignatureError {}
84
85pub fn encode(binding: &RequestBinding<'_>) -> Vec<u8> {
86    let variable_length = binding.server_id.len()
87        + binding.public_origin.len()
88        + binding.username.as_str().len()
89        + binding.method.len()
90        + binding.target.len()
91        + binding.content_type.len();
92    let mut encoded = Vec::with_capacity(FIXED_ENCODED_LENGTH + variable_length);
93    encoded.extend_from_slice(MAGIC);
94    append_string(&mut encoded, binding.server_id);
95    append_string(&mut encoded, binding.public_origin);
96    append_string(&mut encoded, binding.username.as_str());
97    encoded.extend_from_slice(&binding.epoch.to_be_bytes());
98    encoded.extend_from_slice(&binding.nonce);
99    append_string(&mut encoded, binding.method);
100    append_string(&mut encoded, binding.target);
101    append_string(&mut encoded, binding.content_type);
102    encoded.extend_from_slice(&binding.body_sha256);
103    encoded
104}
105
106pub fn verify(
107    binding: &RequestBinding<'_>,
108    public_key: &[u8; 32],
109    signature: &[u8; 64],
110) -> Result<(), SignatureError> {
111    let verifying_key =
112        VerifyingKey::from_bytes(public_key).map_err(|_| SignatureError::MalformedPublicKey)?;
113    let parsed_signature = Signature::from_bytes(signature);
114    verifying_key
115        .verify_strict(&encode(binding), &parsed_signature)
116        .map_err(|_| SignatureError::VerificationFailed)
117}
118
119fn append_string(encoded: &mut Vec<u8>, value: &str) {
120    let length = u64::try_from(value.len()).expect("string length must fit in a u64");
121    encoded.extend_from_slice(&length.to_be_bytes());
122    encoded.extend_from_slice(value.as_bytes());
123}