Skip to main content

edda_auth/
cert.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::{AuthError, DeviceKey, PublicKey, Purpose, Signature};
6
7/// What a device may do in its campaign.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
10#[serde(rename_all = "snake_case")]
11pub enum Role {
12    /// Runs the campaign, and can add and remove devices.
13    Dm,
14    Player,
15}
16
17/// A statement that `device` belongs to `campaign` with `role`, signed by
18/// `issuer`: a DM device of that campaign, or for the very first DM device
19/// (set up with a one-time code) the device itself.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22pub struct DeviceCert {
23    pub campaign: Uuid,
24    pub device: PublicKey,
25    pub role: Role,
26    /// Whose device it is, for people: "Vex's laptop".
27    pub name: String,
28    /// Whole seconds since 1970, so the signed encoding is a plain integer.
29    #[serde(with = "chrono::serde::ts_seconds")]
30    #[cfg_attr(feature = "openapi", schema(value_type = i64))]
31    pub issued_at: DateTime<Utc>,
32    pub issuer: PublicKey,
33}
34
35/// A [`DeviceCert`] with its issuer's signature.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
38pub struct SignedCert {
39    pub cert: DeviceCert,
40    pub signature: Signature,
41}
42
43impl DeviceCert {
44    /// `issuer` vouches for `device`.
45    pub fn issue(
46        issuer: &DeviceKey,
47        campaign: Uuid,
48        device: PublicKey,
49        role: Role,
50        name: impl Into<String>,
51    ) -> Result<SignedCert, AuthError> {
52        let cert = DeviceCert {
53            campaign,
54            device,
55            role,
56            name: name.into(),
57            issued_at: DateTime::from_timestamp(Utc::now().timestamp(), 0).expect("now is valid"),
58            issuer: issuer.public(),
59        };
60        let signature = issuer.sign(Purpose::DeviceCert, &cert)?;
61        Ok(SignedCert { cert, signature })
62    }
63
64    /// A device vouching for itself, which also proves it holds its key.
65    pub fn self_signed(
66        key: &DeviceKey,
67        campaign: Uuid,
68        role: Role,
69        name: impl Into<String>,
70    ) -> Result<SignedCert, AuthError> {
71        Self::issue(key, campaign, key.public(), role, name)
72    }
73}
74
75impl SignedCert {
76    /// Checks the signature against the certificate's own issuer. Whether
77    /// that issuer may vouch for devices in the campaign is up to the
78    /// caller.
79    pub fn verify(&self) -> Result<&DeviceCert, AuthError> {
80        self.cert
81            .issuer
82            .verify(Purpose::DeviceCert, &self.cert, &self.signature)?;
83        Ok(&self.cert)
84    }
85
86    /// Whether the device signed its own certificate, as a campaign's first
87    /// DM device does.
88    pub fn is_self_signed(&self) -> bool {
89        self.cert.issuer == self.cert.device
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn tampering_with_any_field_breaks_the_signature() {
99        let dm = DeviceKey::generate();
100        let player = DeviceKey::generate().public();
101        let campaign = Uuid::new_v4();
102        let signed =
103            DeviceCert::issue(&dm, campaign, player, Role::Player, "Vex's laptop").unwrap();
104        assert!(signed.verify().is_ok());
105        assert!(!signed.is_self_signed());
106
107        let tampered: [fn(&mut DeviceCert); 4] = [
108            |c| c.role = Role::Dm,
109            |c| c.campaign = Uuid::new_v4(),
110            |c| c.name.push('!'),
111            |c| c.device = DeviceKey::generate().public(),
112        ];
113        for tamper in tampered {
114            let mut forged = signed.clone();
115            tamper(&mut forged.cert);
116            assert_eq!(forged.verify(), Err(AuthError::BadSignature));
117        }
118        // Claiming another issuer doesn't help either.
119        let mut forged = signed.clone();
120        forged.cert.issuer = DeviceKey::generate().public();
121        assert_eq!(forged.verify(), Err(AuthError::BadSignature));
122    }
123
124    #[test]
125    fn certificates_survive_json() {
126        let key = DeviceKey::generate();
127        let signed = DeviceCert::self_signed(&key, Uuid::new_v4(), Role::Dm, "DM laptop").unwrap();
128        assert!(signed.is_self_signed());
129        let json = serde_json::to_string(&signed).unwrap();
130        let back: SignedCert = serde_json::from_str(&json).unwrap();
131        assert_eq!(back, signed);
132        assert!(back.verify().is_ok());
133    }
134}