Skip to main content

kc/
cssm.rs

1//! CSSM structures that appear inside keychain blobs, as types.
2//!
3//! These are the fixed-layout C structs from Apple's CDSA headers
4//! (`CSSM_KEYHEADER`, `CSSM_GUID`, `CSSM_DATE`) and `KeyBlob::WrappedFields`
5//! from `ssblob.h`. They are modelled field by field rather than carried as
6//! opaque bytes, so a key blob written here can be compared with one macOS wrote
7//! field by field, and so the values this code chooses are visible in the source
8//! instead of hidden in a hex literal.
9
10use crate::error::{Error, Result};
11
12/// `CSSM_GUID`. Stored big-endian in a keychain, unlike the host-order form the
13/// CSSM API uses in memory.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Guid {
16    pub data1: u32,
17    pub data2: u16,
18    pub data3: u16,
19    pub data4: [u8; 8],
20}
21
22impl Guid {
23    pub const LEN: usize = 16;
24
25    /// The Apple CSP, which is what wraps every item key in a keychain.
26    pub const APPLE_CSP: Self = Self {
27        data1: 0x8719_1ca2,
28        data2: 0x0fc9,
29        data3: 0x11d4,
30        data4: [0x84, 0x9a, 0x00, 0x05, 0x02, 0xb5, 0x21, 0x22],
31    };
32
33    pub fn parse(data: &[u8]) -> Result<Self> {
34        if data.len() < Self::LEN {
35            return Err(Error::Crypto("GUID is truncated"));
36        }
37        Ok(Self {
38            data1: be32(data, 0),
39            data2: u16::from_be_bytes([data[4], data[5]]),
40            data3: u16::from_be_bytes([data[6], data[7]]),
41            data4: data[8..16].try_into().expect("8 bytes"),
42        })
43    }
44
45    pub fn write(&self, out: &mut Vec<u8>) {
46        out.extend_from_slice(&self.data1.to_be_bytes());
47        out.extend_from_slice(&self.data2.to_be_bytes());
48        out.extend_from_slice(&self.data3.to_be_bytes());
49        out.extend_from_slice(&self.data4);
50    }
51}
52
53impl std::fmt::Display for Guid {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(
56            f,
57            "{:08x}-{:04x}-{:04x}-",
58            self.data1, self.data2, self.data3
59        )?;
60        for byte in &self.data4 {
61            write!(f, "{byte:02x}")?;
62        }
63        Ok(())
64    }
65}
66
67/// `CSSM_DATE`: eight ASCII digits, `YYYYMMDD`, or all zeroes for "unset".
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub struct CssmDate(pub [u8; 8]);
70
71impl CssmDate {
72    pub const LEN: usize = 8;
73
74    pub fn is_unset(&self) -> bool {
75        self.0 == [0u8; 8]
76    }
77}
78
79/// `CSSM_KEYCLASS`.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum KeyClass {
82    Public,
83    Private,
84    Session,
85    SecretPart,
86    Other(u32),
87}
88
89impl KeyClass {
90    pub fn from_u32(value: u32) -> Self {
91        match value {
92            0 => Self::Public,
93            1 => Self::Private,
94            2 => Self::Session,
95            3 => Self::SecretPart,
96            other => Self::Other(other),
97        }
98    }
99
100    pub fn as_u32(self) -> u32 {
101        match self {
102            Self::Public => 0,
103            Self::Private => 1,
104            Self::Session => 2,
105            Self::SecretPart => 3,
106            Self::Other(other) => other,
107        }
108    }
109}
110
111/// `CSSM_KEYBLOB_TYPE`.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum KeyBlobType {
114    Raw,
115    Reference,
116    Wrapped,
117    Other(u32),
118}
119
120impl KeyBlobType {
121    pub fn from_u32(value: u32) -> Self {
122        match value {
123            0 => Self::Raw,
124            1 => Self::Reference,
125            2 => Self::Wrapped,
126            other => Self::Other(other),
127        }
128    }
129
130    pub fn as_u32(self) -> u32 {
131        match self {
132            Self::Raw => 0,
133            Self::Reference => 1,
134            Self::Wrapped => 2,
135            Self::Other(other) => other,
136        }
137    }
138}
139
140/// The `CSSM_ALGORITHMS` values this code needs to name.
141pub mod algorithm {
142    /// `CSSM_ALGID_3DES_3KEY_EDE`, the algorithm every keychain item key uses.
143    pub const TRIPLE_DES_3KEY: u32 = 0x11;
144    /// `CSSM_ALGID_RSA`.
145    pub const RSA: u32 = 42;
146    pub const NONE: u32 = 0;
147}
148
149/// `CSSM_ENCRYPT_MODE` values used here.
150pub mod encrypt_mode {
151    pub const NONE: u32 = 0;
152    /// `CSSM_ALGMODE_CBCPadIV8`, the mode the key wrapping uses.
153    pub const CBC_PAD_IV8: u32 = 6;
154}
155
156/// `CSSM_KEYATTR_FLAGS` bits.
157pub mod key_attr {
158    pub const PERMANENT: u32 = 0x0000_0001;
159    pub const PRIVATE: u32 = 0x0000_0002;
160    pub const MODIFIABLE: u32 = 0x0000_0004;
161    pub const SENSITIVE: u32 = 0x0000_0008;
162    pub const EXTRACTABLE: u32 = 0x0000_0010;
163    pub const ALWAYS_SENSITIVE: u32 = 0x0000_0020;
164    pub const NEVER_EXTRACTABLE: u32 = 0x0000_0040;
165}
166
167/// `CSSM_KEYUSE` bits.
168pub mod key_usage {
169    pub const ENCRYPT: u32 = 0x0000_0001;
170    pub const DECRYPT: u32 = 0x0000_0002;
171    /// `CSSM_KEYUSE_ANY`, which macOS records for an imported private key.
172    pub const ANY: u32 = 0x8000_0000;
173}
174
175/// `CSSM_KEYBLOB_FORMAT` values used here.
176pub mod key_format {
177    pub const NONE: u32 = 0;
178    /// `CSSM_KEYBLOB_WRAPPED_FORMAT_APPLE_CUSTOM`.
179    pub const APPLE_CUSTOM: u32 = 0x64;
180}
181
182/// `CSSM_KEYHEADER`, the 76-byte header of every stored key.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct KeyHeader {
185    pub header_version: u32,
186    pub csp_id: Guid,
187    pub blob_type: KeyBlobType,
188    pub format: u32,
189    pub algorithm_id: u32,
190    pub key_class: KeyClass,
191    pub logical_key_size_in_bits: u32,
192    /// `CSSM_KEYATTR_FLAGS`.
193    pub key_attr: u32,
194    /// `CSSM_KEYUSE`.
195    pub key_usage: u32,
196    pub start_date: CssmDate,
197    pub end_date: CssmDate,
198    pub wrap_algorithm_id: u32,
199    pub wrap_mode: u32,
200    pub reserved: u32,
201}
202
203impl KeyHeader {
204    pub const LEN: usize = 76;
205
206    pub fn parse(data: &[u8]) -> Result<Self> {
207        if data.len() < Self::LEN {
208            return Err(Error::Crypto("key header is truncated"));
209        }
210        Ok(Self {
211            header_version: be32(data, 0),
212            csp_id: Guid::parse(&data[4..20])?,
213            blob_type: KeyBlobType::from_u32(be32(data, 20)),
214            format: be32(data, 24),
215            algorithm_id: be32(data, 28),
216            key_class: KeyClass::from_u32(be32(data, 32)),
217            logical_key_size_in_bits: be32(data, 36),
218            key_attr: be32(data, 40),
219            key_usage: be32(data, 44),
220            start_date: CssmDate(data[48..56].try_into().expect("8 bytes")),
221            end_date: CssmDate(data[56..64].try_into().expect("8 bytes")),
222            wrap_algorithm_id: be32(data, 64),
223            wrap_mode: be32(data, 68),
224            reserved: be32(data, 72),
225        })
226    }
227
228    pub fn write(&self, out: &mut Vec<u8>) {
229        out.extend_from_slice(&self.header_version.to_be_bytes());
230        self.csp_id.write(out);
231        out.extend_from_slice(&self.blob_type.as_u32().to_be_bytes());
232        out.extend_from_slice(&self.format.to_be_bytes());
233        out.extend_from_slice(&self.algorithm_id.to_be_bytes());
234        out.extend_from_slice(&self.key_class.as_u32().to_be_bytes());
235        out.extend_from_slice(&self.logical_key_size_in_bits.to_be_bytes());
236        out.extend_from_slice(&self.key_attr.to_be_bytes());
237        out.extend_from_slice(&self.key_usage.to_be_bytes());
238        out.extend_from_slice(&self.start_date.0);
239        out.extend_from_slice(&self.end_date.0);
240        out.extend_from_slice(&self.wrap_algorithm_id.to_be_bytes());
241        out.extend_from_slice(&self.wrap_mode.to_be_bytes());
242        out.extend_from_slice(&self.reserved.to_be_bytes());
243    }
244
245    pub fn to_bytes(&self) -> Vec<u8> {
246        let mut out = Vec::with_capacity(Self::LEN);
247        self.write(&mut out);
248        out
249    }
250
251    /// The header macOS writes for a wrapped item key: a 192-bit 3DES session
252    /// key from the Apple CSP that may only encrypt and decrypt its item.
253    ///
254    /// securityd checks these bits before it will use the key, so they are the
255    /// values `security` writes, not a plausible-looking set.
256    /// `key_size_matches_apples_header` pins the whole header to a keychain macOS
257    /// wrote.
258    pub fn item_key() -> Self {
259        Self {
260            header_version: 2,
261            csp_id: Guid::APPLE_CSP,
262            blob_type: KeyBlobType::Wrapped,
263            format: key_format::NONE,
264            algorithm_id: algorithm::TRIPLE_DES_3KEY,
265            key_class: KeyClass::Session,
266            logical_key_size_in_bits: 192,
267            // Both EXTRACTABLE and NEVER_EXTRACTABLE are set, which is what
268            // macOS writes; the record attributes carry the effective values.
269            key_attr: key_attr::PERMANENT
270                | key_attr::SENSITIVE
271                | key_attr::EXTRACTABLE
272                | key_attr::NEVER_EXTRACTABLE,
273            key_usage: key_usage::ENCRYPT | key_usage::DECRYPT,
274            start_date: CssmDate::default(),
275            end_date: CssmDate::default(),
276            wrap_algorithm_id: algorithm::NONE,
277            wrap_mode: encrypt_mode::NONE,
278            reserved: 0,
279        }
280    }
281}
282
283impl KeyHeader {
284    /// The header macOS writes for an imported RSA private key.
285    ///
286    /// The bits are the ones `security import` writes: permanent, sensitive,
287    /// always sensitive, extractable, and usable for anything.
288    ///
289    /// `key_size` is recorded in the record's attributes as well; the header
290    /// carries the logical size.
291    pub fn private_key(key_size_in_bits: u32) -> Self {
292        Self {
293            algorithm_id: algorithm::RSA,
294            key_class: KeyClass::Private,
295            logical_key_size_in_bits: key_size_in_bits,
296            // Permanent, sensitive and extractable: an imported key.
297            key_attr: key_attr::PERMANENT
298                | key_attr::SENSITIVE
299                | key_attr::EXTRACTABLE
300                | key_attr::ALWAYS_SENSITIVE,
301            key_usage: key_usage::ANY,
302            ..Self::item_key()
303        }
304    }
305}
306
307/// `KeyBlob::WrappedFields`: how the key that follows was wrapped.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct WrappedKeyFields {
310    pub blob_type: u32,
311    pub blob_format: u32,
312    pub wrap_algorithm: u32,
313    pub wrap_mode: u32,
314}
315
316impl WrappedKeyFields {
317    pub const LEN: usize = 16;
318
319    pub fn parse(data: &[u8]) -> Result<Self> {
320        if data.len() < Self::LEN {
321            return Err(Error::Crypto("wrapped-key fields are truncated"));
322        }
323        Ok(Self {
324            blob_type: be32(data, 0),
325            blob_format: be32(data, 4),
326            wrap_algorithm: be32(data, 8),
327            wrap_mode: be32(data, 12),
328        })
329    }
330
331    pub fn write(&self, out: &mut Vec<u8>) {
332        out.extend_from_slice(&self.blob_type.to_be_bytes());
333        out.extend_from_slice(&self.blob_format.to_be_bytes());
334        out.extend_from_slice(&self.wrap_algorithm.to_be_bytes());
335        out.extend_from_slice(&self.wrap_mode.to_be_bytes());
336    }
337
338    /// What macOS records for an item key wrapped in its custom format.
339    pub fn item_key() -> Self {
340        Self {
341            blob_type: 3,
342            blob_format: key_format::APPLE_CUSTOM,
343            wrap_algorithm: algorithm::TRIPLE_DES_3KEY,
344            wrap_mode: encrypt_mode::CBC_PAD_IV8,
345        }
346    }
347}
348
349fn be32(data: &[u8], at: usize) -> u32 {
350    u32::from_be_bytes([data[at], data[at + 1], data[at + 2], data[at + 3]])
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn guid_round_trips_and_prints() {
359        let mut bytes = Vec::new();
360        Guid::APPLE_CSP.write(&mut bytes);
361        assert_eq!(bytes.len(), Guid::LEN);
362        assert_eq!(Guid::parse(&bytes).unwrap(), Guid::APPLE_CSP);
363        assert_eq!(
364            Guid::APPLE_CSP.to_string(),
365            "87191ca2-0fc9-11d4-849a000502b52122"
366        );
367    }
368
369    #[test]
370    fn key_header_round_trips() {
371        let header = KeyHeader::item_key();
372        let bytes = header.to_bytes();
373        assert_eq!(bytes.len(), KeyHeader::LEN);
374        assert_eq!(KeyHeader::parse(&bytes).unwrap(), header);
375    }
376
377    /// The exact bytes macOS writes for an item key's header, transcribed from a
378    /// keychain created by `security add-generic-password`.
379    #[test]
380    fn item_key_header_matches_the_bytes_macos_writes() {
381        assert_eq!(
382            hex::encode(KeyHeader::item_key().to_bytes()),
383            concat!(
384                "00000002",                         // header version
385                "87191ca20fc911d4849a000502b52122", // Apple CSP GUID
386                "00000002",                         // blob type: wrapped
387                "00000000",                         // format
388                "00000011",                         // 3DES-3KEY
389                "00000002",                         // key class: session
390                "000000c0",                         // 192 bits
391                "00000059",                         // key attributes
392                "00000003",                         // usage: encrypt | decrypt
393                "0000000000000000",                 // start date
394                "0000000000000000",                 // end date
395                "00000000",                         // wrap algorithm
396                "00000000",                         // wrap mode
397                "00000000",                         // reserved
398            )
399        );
400    }
401
402    #[test]
403    fn key_header_holds_the_values_a_keychain_item_key_needs() {
404        let header = KeyHeader::item_key();
405        assert_eq!(header.blob_type, KeyBlobType::Wrapped);
406        assert_eq!(header.key_class, KeyClass::Session);
407        assert_eq!(header.algorithm_id, algorithm::TRIPLE_DES_3KEY);
408        assert_eq!(header.logical_key_size_in_bits, 192);
409        assert!(header.start_date.is_unset() && header.end_date.is_unset());
410    }
411
412    #[test]
413    fn wrapped_fields_round_trip() {
414        let fields = WrappedKeyFields::item_key();
415        let mut bytes = Vec::new();
416        fields.write(&mut bytes);
417        assert_eq!(bytes.len(), WrappedKeyFields::LEN);
418        assert_eq!(WrappedKeyFields::parse(&bytes).unwrap(), fields);
419        assert_eq!(fields.blob_format, key_format::APPLE_CUSTOM);
420    }
421
422    #[test]
423    fn enums_round_trip_through_their_wire_values() {
424        for value in 0..=4 {
425            assert_eq!(KeyClass::from_u32(value).as_u32(), value);
426            assert_eq!(KeyBlobType::from_u32(value).as_u32(), value);
427        }
428        assert_eq!(KeyClass::from_u32(99), KeyClass::Other(99));
429    }
430
431    #[test]
432    fn truncated_input_is_an_error_not_a_panic() {
433        assert!(Guid::parse(&[0u8; 8]).is_err());
434        assert!(KeyHeader::parse(&[0u8; 40]).is_err());
435        assert!(WrappedKeyFields::parse(&[0u8; 4]).is_err());
436    }
437}