Skip to main content

bitrouter_core/jwt/
keys.rs

1//! Ed25519 key management for BitRouter JWT authentication.
2//!
3//! Wraps `ed25519-dalek` to provide key generation, serialization, and a
4//! JSON-friendly "master key" format stored at
5//! `BITROUTER_HOME/.keys/<pubkey_prefix>/master.json`.
6
7use base64::Engine;
8use base64::engine::general_purpose::URL_SAFE_NO_PAD;
9use ed25519_dalek::{SigningKey, VerifyingKey};
10use rand::rngs::OsRng;
11use serde::{Deserialize, Serialize};
12
13use crate::jwt::JwtError;
14
15/// An Ed25519 master keypair for signing BitRouter JWTs.
16///
17/// The private key is 64 bytes: 32-byte seed concatenated with 32-byte public key
18/// (standard Ed25519 keypair format). The public key alone is 32 bytes.
19#[derive(Clone)]
20pub struct MasterKeypair {
21    signing_key: SigningKey,
22}
23
24impl MasterKeypair {
25    /// Generate a new random Ed25519 keypair.
26    pub fn generate() -> Self {
27        Self {
28            signing_key: SigningKey::generate(&mut OsRng),
29        }
30    }
31
32    /// Reconstruct from the 64-byte keypair bytes (seed + public key).
33    pub fn from_keypair_bytes(bytes: &[u8; 64]) -> Result<Self, JwtError> {
34        let signing_key =
35            SigningKey::from_keypair_bytes(bytes).map_err(|_| JwtError::InvalidKeypair)?;
36        Ok(Self { signing_key })
37    }
38
39    /// Serialize to the 64-byte keypair format (seed + public key).
40    pub fn to_keypair_bytes(&self) -> [u8; 64] {
41        self.signing_key.to_keypair_bytes()
42    }
43
44    /// Return the signing key (for JWT signing).
45    pub fn signing_key(&self) -> &SigningKey {
46        &self.signing_key
47    }
48
49    /// Return the verifying (public) key.
50    pub fn verifying_key(&self) -> VerifyingKey {
51        self.signing_key.verifying_key()
52    }
53
54    /// The 32-byte public key, base64url-encoded (no padding).
55    /// This is the value used as the `iss` claim in JWTs.
56    pub fn public_key_b64(&self) -> String {
57        URL_SAFE_NO_PAD.encode(self.verifying_key().as_bytes())
58    }
59
60    /// A short prefix of the public key for display and directory naming.
61    /// Returns the first 16 characters of the base64url-encoded public key.
62    pub fn public_key_prefix(&self) -> String {
63        let b64 = self.public_key_b64();
64        b64[..16.min(b64.len())].to_string()
65    }
66
67    /// Serialize to the JSON format stored in `master.json`.
68    pub fn to_json(&self) -> MasterKeyJson {
69        MasterKeyJson {
70            algorithm: "eddsa".to_string(),
71            secret_key: URL_SAFE_NO_PAD.encode(self.to_keypair_bytes()),
72        }
73    }
74
75    /// Deserialize from the JSON format stored in `master.json`.
76    pub fn from_json(json: &MasterKeyJson) -> Result<Self, JwtError> {
77        if json.algorithm != "eddsa" {
78            return Err(JwtError::InvalidKeypair);
79        }
80        let bytes = URL_SAFE_NO_PAD
81            .decode(&json.secret_key)
82            .map_err(|_| JwtError::InvalidKeypair)?;
83        let bytes: [u8; 64] = bytes.try_into().map_err(|_| JwtError::InvalidKeypair)?;
84        Self::from_keypair_bytes(&bytes)
85    }
86}
87
88/// Decode a base64url-encoded public key string into a `VerifyingKey`.
89pub fn decode_public_key(b64: &str) -> Result<VerifyingKey, JwtError> {
90    let bytes = URL_SAFE_NO_PAD
91        .decode(b64)
92        .map_err(|_| JwtError::InvalidPublicKey)?;
93    let bytes: [u8; 32] = bytes.try_into().map_err(|_| JwtError::InvalidPublicKey)?;
94    VerifyingKey::from_bytes(&bytes).map_err(|_| JwtError::InvalidPublicKey)
95}
96
97/// JSON-serializable format for the master key file.
98///
99/// Stored at `BITROUTER_HOME/.keys/<pubkey_prefix>/master.json`.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct MasterKeyJson {
102    /// Algorithm identifier. Always "eddsa" for Ed25519.
103    pub algorithm: String,
104    /// The 64-byte keypair (seed + public key), base64url-encoded.
105    pub secret_key: String,
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn roundtrip_keypair() {
114        let kp = MasterKeypair::generate();
115        let bytes = kp.to_keypair_bytes();
116        let kp2 = MasterKeypair::from_keypair_bytes(&bytes).expect("valid keypair");
117        assert_eq!(kp.public_key_b64(), kp2.public_key_b64());
118    }
119
120    #[test]
121    fn roundtrip_json() {
122        let kp = MasterKeypair::generate();
123        let json = kp.to_json();
124        let kp2 = MasterKeypair::from_json(&json).expect("valid json");
125        assert_eq!(kp.public_key_b64(), kp2.public_key_b64());
126    }
127
128    #[test]
129    fn public_key_prefix_length() {
130        let kp = MasterKeypair::generate();
131        assert_eq!(kp.public_key_prefix().len(), 16);
132    }
133
134    #[test]
135    fn decode_public_key_roundtrip() {
136        let kp = MasterKeypair::generate();
137        let b64 = kp.public_key_b64();
138        let vk = decode_public_key(&b64).expect("valid public key");
139        assert_eq!(vk, kp.verifying_key());
140    }
141
142    #[test]
143    fn from_json_rejects_wrong_algorithm() {
144        let kp = MasterKeypair::generate();
145        let mut json = kp.to_json();
146        json.algorithm = "rsa".to_string();
147        assert!(MasterKeypair::from_json(&json).is_err());
148    }
149}