1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::{AuthError, DeviceKey, PublicKey, Purpose, Signature};
6
7#[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 Dm,
14 Player,
15}
16
17#[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 pub name: String,
28 #[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#[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 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 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 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 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 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}