Skip to main content

bitwarden_pam_sealed/
target_system.rs

1use bitwarden_crypto::{
2    EncString, KeySlotIds, KeyStoreContext, generate_versioned_sealable,
3    safe::{
4        DataEnvelope, DataEnvelopeError, DataEnvelopeNamespace, SealableData, SealableVersionedData,
5    },
6};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::{Sealed, policy::SealedPasswordPolicy};
11
12/// Whether a target's credentials are rotated automatically or by hand.
13#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum SealedTargetSystemMethod {
16    /// Rotated by the connector on a schedule.
17    Automatic,
18    /// The connector never dispatches jobs for this target.
19    Manual,
20}
21
22/// Connector plugin that rotates an automatic target.
23#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
24#[serde(rename_all = "snake_case")]
25pub enum SealedTargetSystemKind {
26    /// Microsoft Entra ID.
27    Entra,
28    /// Microsoft SQL Server.
29    Mssql,
30    /// A customer-supplied rotation script.
31    CustomScript,
32}
33
34/// Security-relevant fields of a PAM target system. Sealed so neither a hostile server nor a MitM
35/// can change its kind, password policy, or session-termination support.
36#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
37pub struct TargetSystemSealedV1 {
38    /// Cross-checked against the claim and the rotation config's copy to stop blob swaps.
39    pub target_system_id: Uuid,
40    /// The connector requires this to be `Automatic` before running a job.
41    pub method: SealedTargetSystemMethod,
42    /// `None` for manual targets.
43    pub kind: Option<SealedTargetSystemKind>,
44    /// Policy for the generated credential.
45    pub password_policy: SealedPasswordPolicy,
46    /// `None` for manual targets.
47    pub supports_session_termination: Option<bool>,
48}
49impl SealableData for TargetSystemSealedV1 {}
50
51generate_versioned_sealable!(
52    TargetSystemSealed,
53    DataEnvelopeNamespace::PamTargetSystem,
54    [
55        TargetSystemSealedV1 => "1",
56    ]
57);
58
59/// Seals `data` under `org_key`.
60pub fn seal_target_system<Ids: KeySlotIds>(
61    data: TargetSystemSealedV1,
62    org_key: &Ids::Symmetric,
63    ctx: &mut KeyStoreContext<Ids>,
64) -> Result<Sealed, DataEnvelopeError> {
65    let versioned: TargetSystemSealed = data.into();
66    let (envelope, wrapped_key) = DataEnvelope::seal_with_wrapping_key(versioned, org_key, ctx)?;
67    Ok(Sealed {
68        envelope,
69        wrapped_key,
70    })
71}
72
73/// Unseals a [`TargetSystemSealedV1`] from the output of [`seal_target_system`].
74pub fn unseal_target_system<Ids: KeySlotIds>(
75    envelope: &DataEnvelope,
76    wrapped_key: &EncString,
77    org_key: &Ids::Symmetric,
78    ctx: &mut KeyStoreContext<Ids>,
79) -> Result<TargetSystemSealedV1, DataEnvelopeError> {
80    let versioned: TargetSystemSealed =
81        envelope.unseal_with_wrapping_key(org_key, wrapped_key, ctx)?;
82    let TargetSystemSealed::TargetSystemSealedV1(data) = versioned;
83    Ok(data)
84}
85
86#[cfg(test)]
87mod tests {
88    use std::str::FromStr;
89
90    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, safe::DataEnvelopeError};
91    use bitwarden_encoding::B64;
92
93    use super::*;
94    use crate::{rotation_config::unseal_rotation_config, test_support::TestIds};
95
96    fn sample() -> TargetSystemSealedV1 {
97        TargetSystemSealedV1 {
98            target_system_id: Uuid::parse_str("8c6e7d1a-3e3f-4a5b-9c1d-2f3e4d5c6b7a").unwrap(),
99            method: SealedTargetSystemMethod::Automatic,
100            kind: Some(SealedTargetSystemKind::Entra),
101            password_policy: SealedPasswordPolicy {
102                min_length: 20,
103                max_length: 20,
104                include_uppercase: true,
105                include_lowercase: true,
106                include_digits: true,
107                include_symbols: true,
108            },
109            supports_session_termination: Some(true),
110        }
111    }
112
113    #[test]
114    fn round_trip() {
115        let store = KeyStore::<TestIds>::default();
116        let mut ctx = store.context_mut();
117        let org_key = ctx.generate_symmetric_key();
118
119        let data = sample();
120        let Sealed {
121            envelope,
122            wrapped_key,
123        } = seal_target_system(data.clone(), &org_key, &mut ctx).unwrap();
124        let unsealed = unseal_target_system(&envelope, &wrapped_key, &org_key, &mut ctx).unwrap();
125
126        assert_eq!(unsealed, data);
127    }
128
129    #[test]
130    fn wire_string_round_trip() {
131        let store = KeyStore::<TestIds>::default();
132        let mut ctx = store.context_mut();
133        let org_key = ctx.generate_symmetric_key();
134
135        let data = sample();
136        let Sealed {
137            envelope,
138            wrapped_key,
139        } = seal_target_system(data.clone(), &org_key, &mut ctx).unwrap();
140        let envelope: DataEnvelope = envelope.to_string().parse().unwrap();
141        let wrapped_key: EncString = wrapped_key.to_string().parse().unwrap();
142
143        let unsealed = unseal_target_system(&envelope, &wrapped_key, &org_key, &mut ctx).unwrap();
144        assert_eq!(unsealed, data);
145    }
146
147    #[test]
148    fn wrong_key_fails() {
149        let store = KeyStore::<TestIds>::default();
150        let mut ctx = store.context_mut();
151        let org_key = ctx.generate_symmetric_key();
152        let wrong_key = ctx.generate_symmetric_key();
153
154        let Sealed {
155            envelope,
156            wrapped_key,
157        } = seal_target_system(sample(), &org_key, &mut ctx).unwrap();
158        let result = unseal_target_system::<TestIds>(&envelope, &wrapped_key, &wrong_key, &mut ctx);
159
160        assert!(result.is_err());
161    }
162
163    #[test]
164    fn target_system_blob_cannot_be_unsealed_as_rotation_config() {
165        let store = KeyStore::<TestIds>::default();
166        let mut ctx = store.context_mut();
167        let org_key = ctx.generate_symmetric_key();
168
169        let Sealed {
170            envelope,
171            wrapped_key,
172        } = seal_target_system(sample(), &org_key, &mut ctx).unwrap();
173        let result = unseal_rotation_config::<TestIds>(&envelope, &wrapped_key, &org_key, &mut ctx);
174
175        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
176    }
177
178    #[test]
179    fn swapped_wrapped_key_fails() {
180        let store = KeyStore::<TestIds>::default();
181        let mut ctx = store.context_mut();
182        let org_key = ctx.generate_symmetric_key();
183
184        let Sealed { envelope, .. } = seal_target_system(sample(), &org_key, &mut ctx).unwrap();
185        let Sealed {
186            wrapped_key: other_wrapped_key,
187            ..
188        } = seal_target_system(sample(), &org_key, &mut ctx).unwrap();
189        let result =
190            unseal_target_system::<TestIds>(&envelope, &other_wrapped_key, &org_key, &mut ctx);
191
192        assert!(result.is_err());
193    }
194
195    #[test]
196    #[ignore = "Manual test to generate test vectors"]
197    fn generate_test_vectors() {
198        let store = KeyStore::<TestIds>::default();
199        let mut ctx = store.context_mut();
200        let org_key = ctx.generate_symmetric_key();
201        let key_b64 = {
202            #[allow(deprecated)]
203            let key = ctx.dangerous_get_symmetric_key(org_key).unwrap();
204            key.to_base64()
205        };
206
207        let Sealed {
208            envelope,
209            wrapped_key,
210        } = seal_target_system(sample(), &org_key, &mut ctx).unwrap();
211
212        let (envelope, wrapped_key) = (envelope.to_string(), wrapped_key.to_string());
213        println!("const TEST_VECTOR_ORG_KEY: &str = \"{key_b64}\";");
214        println!("const TEST_VECTOR_TARGET_SYSTEM_SEALED_DATA: &str = \"{envelope}\";");
215        println!("const TEST_VECTOR_TARGET_SYSTEM_WRAPPED_KEY: &str = \"{wrapped_key}\";");
216    }
217
218    const TEST_VECTOR_ORG_KEY: &str =
219        "MUgcMToL6hTXhQSr3bQuvJDw+P4y9vlRlvEQ36yFlgErQ0kdL5Xh3U92i/P3uJVk7X6ujV/uWO9usbo/X+0YcA==";
220    const TEST_VECTOR_TARGET_SYSTEM_SEALED_DATA: &str = "g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUHRhqF0LBJ8L12LJH4jFX186AAE4gQI6AAE4gAShBUxjXNBlDL/mPqel7U1Y8r2jmj4RoevZ2EkAV+RTDBLzvGAH6a0YBM8wm9G72AK+rpeVpLrQrFnAimeptb+OJj0Y5Wd2aC5ZZaRfeAzISQeSOGSk6M6Hfdn3YZR0RiJXbggAAc+rs0Zi7zSbfzffpmAfLxGuNG8tCEgInq8KijZnK1p8ff6uCEcNMhstzt5uFC3R1Mm/ua+iNiaYqXAZZLGn1vKQ8f4IC2G61SYtmgN70iZjea4HCad48Lrp16ny1/OAxAsR9iJgMTOHVDGXrAerGBKcJiho0My6CwVPQmQNHgNN1Ki/gvflT1FiYWFkiHat09pOfgGoDyjc3X8dbcgy";
221    const TEST_VECTOR_TARGET_SYSTEM_WRAPPED_KEY: &str = "2.WakihY9ZIyZBuvrrvELJiQ==|j4sTuFZ7BqvcHiGKGcuI2tTjf3sZv29iUvh/I0eO/3EHzCuxsrEajOdZOneS9w1Hyag8agcO2IjmEOc0t3baBSpFqqZrmy/5UJ99n/zE3gU=|+DDgtyBastM+rPThuzq2ec4xa1ejGZMJaijzwf7aK9w=";
222
223    #[test]
224    fn target_system_test_vector() {
225        let org_key =
226            SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_ORG_KEY).unwrap()).unwrap();
227
228        let store = KeyStore::<TestIds>::default();
229        let mut ctx = store.context_mut();
230        let org_key_id = ctx.add_local_symmetric_key(org_key);
231
232        let envelope = DataEnvelope::from_str(TEST_VECTOR_TARGET_SYSTEM_SEALED_DATA).unwrap();
233        let wrapped_key = EncString::from_str(TEST_VECTOR_TARGET_SYSTEM_WRAPPED_KEY).unwrap();
234        let unsealed = unseal_target_system(&envelope, &wrapped_key, &org_key_id, &mut ctx).expect(
235            "TargetSystemSealed wire format has changed in a backwards-incompatible way. \
236                 Existing sealed data must remain deserializable.",
237        );
238
239        assert_eq!(unsealed, sample());
240    }
241}