Skip to main content

opentdf_protocol/nanotdf/
policy.rs

1//! NanoTDF Policy Structures
2
3use crate::binary::{BinaryRead, BinaryWrite, read_u8, read_u16_be, write_u8, write_u16_be};
4use crate::nanotdf::resource_locator::ResourceLocator;
5use std::io::{self, Read, Write};
6
7/// Policy type indicator
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[repr(u8)]
10pub enum PolicyType {
11    /// Remote policy (reference via Resource Locator)
12    Remote = 0x00,
13    /// Embedded policy (plaintext)
14    EmbeddedPlaintext = 0x01,
15    /// Embedded policy (encrypted)
16    EmbeddedEncrypted = 0x02,
17    /// Embedded policy (encrypted with separate key access)
18    EmbeddedEncryptedPolicyKeyAccess = 0x03,
19}
20
21impl PolicyType {
22    /// Parse from byte
23    pub fn from_byte(byte: u8) -> io::Result<Self> {
24        match byte {
25            0x00 => Ok(PolicyType::Remote),
26            0x01 => Ok(PolicyType::EmbeddedPlaintext),
27            0x02 => Ok(PolicyType::EmbeddedEncrypted),
28            0x03 => Ok(PolicyType::EmbeddedEncryptedPolicyKeyAccess),
29            _ => Err(io::Error::new(
30                io::ErrorKind::InvalidData,
31                format!("Invalid policy type: 0x{:02X}", byte),
32            )),
33        }
34    }
35
36    /// Convert to byte
37    pub fn to_byte(self) -> u8 {
38        self as u8
39    }
40}
41
42/// Policy body variants
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum PolicyBody {
45    /// Remote policy reference
46    Remote(ResourceLocator),
47    /// Embedded plaintext policy
48    EmbeddedPlaintext(Vec<u8>),
49    /// Embedded encrypted policy (uses reserved IV 0x000000)
50    EmbeddedEncrypted(Vec<u8>),
51    /// Embedded encrypted policy with separate key access
52    EmbeddedEncryptedWithKeyAccess {
53        /// Encrypted policy content
54        content: Vec<u8>,
55        /// KAS resource locator for policy key
56        key_access: ResourceLocator,
57        /// Ephemeral public key for policy key derivation
58        ephemeral_key: Vec<u8>,
59    },
60}
61
62impl PolicyBody {
63    /// Get the policy type
64    pub fn policy_type(&self) -> PolicyType {
65        match self {
66            PolicyBody::Remote(_) => PolicyType::Remote,
67            PolicyBody::EmbeddedPlaintext(_) => PolicyType::EmbeddedPlaintext,
68            PolicyBody::EmbeddedEncrypted(_) => PolicyType::EmbeddedEncrypted,
69            PolicyBody::EmbeddedEncryptedWithKeyAccess { .. } => {
70                PolicyType::EmbeddedEncryptedPolicyKeyAccess
71            }
72        }
73    }
74}
75
76/// Policy with cryptographic binding
77///
78/// The policy can be:
79/// - Remote (referenced by URL)
80/// - Embedded plaintext
81/// - Embedded encrypted
82/// - Embedded encrypted with separate key access
83///
84/// All policies include a binding that cryptographically ties
85/// the policy to the payload encryption key.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Policy {
88    /// Policy content
89    pub body: PolicyBody,
90    /// Cryptographic binding (GMAC tag or ECDSA signature)
91    /// Size depends on binding mode:
92    /// - GMAC: 64 bits = 8 bytes
93    /// - ECDSA: curve-dependent (64-132 bytes for r,s)
94    pub binding: Vec<u8>,
95}
96
97impl Policy {
98    /// Create a new policy
99    pub fn new(body: PolicyBody, binding: Vec<u8>) -> Self {
100        Self { body, binding }
101    }
102
103    /// Create a remote policy
104    pub fn remote(locator: ResourceLocator, binding: Vec<u8>) -> Self {
105        Self::new(PolicyBody::Remote(locator), binding)
106    }
107
108    /// Create an embedded plaintext policy
109    pub fn embedded_plaintext(content: Vec<u8>, binding: Vec<u8>) -> Self {
110        Self::new(PolicyBody::EmbeddedPlaintext(content), binding)
111    }
112
113    /// Create an embedded encrypted policy
114    pub fn embedded_encrypted(content: Vec<u8>, binding: Vec<u8>) -> Self {
115        Self::new(PolicyBody::EmbeddedEncrypted(content), binding)
116    }
117}
118
119impl BinaryRead for Policy {
120    fn read_from<R: Read>(reader: &mut R) -> io::Result<Self> {
121        // Read policy type
122        let policy_type = PolicyType::from_byte(read_u8(reader)?)?;
123
124        // Read body based on type
125        let body = match policy_type {
126            PolicyType::Remote => {
127                let locator = ResourceLocator::read_from(reader)?;
128                PolicyBody::Remote(locator)
129            }
130            PolicyType::EmbeddedPlaintext | PolicyType::EmbeddedEncrypted => {
131                // Read content length (2 bytes)
132                let content_len = read_u16_be(reader)? as usize;
133
134                // Read content
135                let mut content = vec![0u8; content_len];
136                reader.read_exact(&mut content)?;
137
138                if policy_type == PolicyType::EmbeddedPlaintext {
139                    PolicyBody::EmbeddedPlaintext(content)
140                } else {
141                    PolicyBody::EmbeddedEncrypted(content)
142                }
143            }
144            PolicyType::EmbeddedEncryptedPolicyKeyAccess => {
145                // Read content length (2 bytes)
146                let content_len = read_u16_be(reader)? as usize;
147
148                // Read content
149                let mut content = vec![0u8; content_len];
150                reader.read_exact(&mut content)?;
151
152                // Read key access resource locator
153                let key_access = ResourceLocator::read_from(reader)?;
154
155                // Read ephemeral public key
156                // Note: We need to know the ECC mode from the header to know the size
157                // For now, we'll read a P-256 key (33 bytes) as default
158                // This will need to be refactored to pass ECC mode from header
159                let mut ephemeral_key = vec![0u8; 33];
160                reader.read_exact(&mut ephemeral_key)?;
161
162                PolicyBody::EmbeddedEncryptedWithKeyAccess {
163                    content,
164                    key_access,
165                    ephemeral_key,
166                }
167            }
168        };
169
170        // Read binding
171        // L1L v12 format: Always 8 bytes (SHA-256 last 8 bytes)
172        // Note: This should eventually be refactored to pass binding mode from header
173        // for ECDSA support (which has variable size)
174        let mut binding = vec![0u8; 8];
175        reader.read_exact(&mut binding)?;
176
177        Ok(Self { body, binding })
178    }
179}
180
181impl BinaryWrite for Policy {
182    fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
183        // Write policy type
184        write_u8(writer, self.body.policy_type().to_byte())?;
185
186        // Write body based on type
187        match &self.body {
188            PolicyBody::Remote(locator) => {
189                locator.write_to(writer)?;
190            }
191            PolicyBody::EmbeddedPlaintext(content) | PolicyBody::EmbeddedEncrypted(content) => {
192                // Validate content length
193                if content.len() > 0xFFFF {
194                    return Err(io::Error::new(
195                        io::ErrorKind::InvalidInput,
196                        format!(
197                            "Policy content too large: {} bytes (max 65535)",
198                            content.len()
199                        ),
200                    ));
201                }
202
203                // Write content length (2 bytes)
204                write_u16_be(writer, content.len() as u16)?;
205
206                // Write content
207                writer.write_all(content)?;
208            }
209            PolicyBody::EmbeddedEncryptedWithKeyAccess {
210                content,
211                key_access,
212                ephemeral_key,
213            } => {
214                // Validate content length
215                if content.len() > 0xFFFF {
216                    return Err(io::Error::new(
217                        io::ErrorKind::InvalidInput,
218                        format!(
219                            "Policy content too large: {} bytes (max 65535)",
220                            content.len()
221                        ),
222                    ));
223                }
224
225                // Write content length (2 bytes)
226                write_u16_be(writer, content.len() as u16)?;
227
228                // Write content
229                writer.write_all(content)?;
230
231                // Write key access resource locator
232                key_access.write_to(writer)?;
233
234                // Write ephemeral public key
235                writer.write_all(ephemeral_key)?;
236            }
237        }
238
239        // Write binding
240        writer.write_all(&self.binding)?;
241
242        Ok(())
243    }
244
245    fn serialized_size(&self) -> usize {
246        let body_size = match &self.body {
247            PolicyBody::Remote(locator) => locator.serialized_size(),
248            PolicyBody::EmbeddedPlaintext(content) | PolicyBody::EmbeddedEncrypted(content) => {
249                2 + content.len() // 2 bytes length + content
250            }
251            PolicyBody::EmbeddedEncryptedWithKeyAccess {
252                content,
253                key_access,
254                ephemeral_key,
255            } => 2 + content.len() + key_access.serialized_size() + ephemeral_key.len(),
256        };
257
258        1 + // Policy type
259        body_size +
260        self.binding.len()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::nanotdf::resource_locator::Protocol;
268
269    #[test]
270    fn test_remote_policy() {
271        let locator = ResourceLocator::new(Protocol::Https, b"kas.example.com/policy/123");
272        let binding = vec![0u8; 8]; // GMAC binding
273        let policy = Policy::remote(locator, binding);
274
275        assert!(matches!(policy.body, PolicyBody::Remote(_)));
276        assert_eq!(policy.body.policy_type(), PolicyType::Remote);
277    }
278
279    #[test]
280    fn test_embedded_plaintext_policy() {
281        let content = b"policy content".to_vec();
282        let binding = vec![0u8; 8];
283        let policy = Policy::embedded_plaintext(content.clone(), binding);
284
285        match &policy.body {
286            PolicyBody::EmbeddedPlaintext(c) => assert_eq!(c, &content),
287            _ => panic!("Expected EmbeddedPlaintext"),
288        }
289    }
290
291    #[test]
292    fn test_policy_roundtrip() {
293        let content = b"test policy".to_vec();
294        let binding = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
295        let original = Policy::embedded_plaintext(content, binding);
296
297        let mut buf = Vec::new();
298        original.write_to(&mut buf).unwrap();
299
300        // Note: This test will fail until we properly handle binding size in read_from
301        // For now, it demonstrates the write path works
302        assert!(!buf.is_empty());
303    }
304}