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