1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
2use serde::{Deserialize, Serialize};
3
4use crate::{ContentDigest, RegistrySourceId};
5
6pub const ENCRYPTED_ARTIFACT_SCHEMA_VERSION: u32 = 1;
7pub const PACKAGE_KEY_ENVELOPE_SCHEMA_VERSION: u32 = 1;
8pub const P256_PUBLIC_KEY_BYTE_LEN: usize = 65;
9pub const AES_256_GCM_NONCE_BYTE_LEN: usize = 12;
10pub const AES_256_GCM_WRAPPED_KEY_BYTE_LEN: usize = 48;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum ArtifactContentCipher {
15 Aes256Gcm,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum RecipientKeyAlgorithm {
21 P256,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "kebab-case")]
26pub enum KeyEnvelopeAlgorithm {
27 P256HkdfSha256Aes256Gcm,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase", deny_unknown_fields)]
32pub struct RecipientEncryptionKey {
33 pub id: String,
34 pub algorithm: RecipientKeyAlgorithm,
35 pub public_key: String,
36 pub fingerprint: ContentDigest,
37}
38
39impl RecipientEncryptionKey {
40 pub fn validate(&self) -> Result<(), String> {
41 if self.id.trim().is_empty() || self.id.len() > 128 {
42 return Err("recipient encryption key id is invalid".to_string());
43 }
44 let public_key = decode_exact(
45 &self.public_key,
46 P256_PUBLIC_KEY_BYTE_LEN,
47 "recipient encryption public key",
48 )?;
49 if p256::PublicKey::from_sec1_bytes(&public_key).is_err()
50 || self.fingerprint != ContentDigest::sha256(&public_key)
51 {
52 return Err("recipient encryption public key is invalid".to_string());
53 }
54 Ok(())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase", deny_unknown_fields)]
60pub struct EncryptedArtifactMetadata {
61 pub schema_version: u32,
62 pub content_cipher: ArtifactContentCipher,
63 pub key_version: u64,
64 pub plaintext_digest: ContentDigest,
65 pub plaintext_byte_len: u64,
66 pub nonce: String,
67 pub aad_digest: ContentDigest,
68}
69
70impl EncryptedArtifactMetadata {
71 pub fn new(
72 key_version: u64,
73 plaintext: &[u8],
74 nonce: [u8; AES_256_GCM_NONCE_BYTE_LEN],
75 source: &RegistrySourceId,
76 ) -> Result<Self, String> {
77 let mut value = Self {
78 schema_version: ENCRYPTED_ARTIFACT_SCHEMA_VERSION,
79 content_cipher: ArtifactContentCipher::Aes256Gcm,
80 key_version,
81 plaintext_digest: ContentDigest::sha256(plaintext),
82 plaintext_byte_len: plaintext.len() as u64,
83 nonce: URL_SAFE_NO_PAD.encode(nonce),
84 aad_digest: ContentDigest::sha256([]),
85 };
86 value.aad_digest = ContentDigest::sha256(value.aad_bytes(source)?);
87 value.validate(source)?;
88 Ok(value)
89 }
90
91 pub fn validate(&self, source: &RegistrySourceId) -> Result<(), String> {
92 if self.schema_version != ENCRYPTED_ARTIFACT_SCHEMA_VERSION
93 || self.key_version == 0
94 || self.plaintext_byte_len == 0
95 {
96 return Err("encrypted artifact metadata is invalid".to_string());
97 }
98 decode_exact(
99 &self.nonce,
100 AES_256_GCM_NONCE_BYTE_LEN,
101 "encrypted artifact nonce",
102 )?;
103 if self.aad_digest != ContentDigest::sha256(self.aad_bytes(source)?) {
104 return Err("encrypted artifact AAD digest is invalid".to_string());
105 }
106 Ok(())
107 }
108
109 pub fn aad_bytes(&self, source: &RegistrySourceId) -> Result<Vec<u8>, String> {
110 #[derive(Serialize)]
111 #[serde(rename_all = "camelCase")]
112 struct Canonical<'a> {
113 format: &'static str,
114 registry: &'a str,
115 namespace: &'a str,
116 name: &'a str,
117 version: String,
118 tree_digest: String,
119 content_cipher: ArtifactContentCipher,
120 key_version: u64,
121 plaintext_digest: String,
122 plaintext_byte_len: u64,
123 }
124 if self.schema_version != ENCRYPTED_ARTIFACT_SCHEMA_VERSION
125 || self.key_version == 0
126 || self.plaintext_byte_len == 0
127 {
128 return Err("encrypted artifact metadata is invalid".to_string());
129 }
130 serde_json::to_vec(&Canonical {
131 format: "runmat-private-artifact-aad-v1",
132 registry: source.registry_origin.as_str(),
133 namespace: source.package.organization(),
134 name: source.package.name(),
135 version: source.version.to_string(),
136 tree_digest: source.tree_digest.to_string(),
137 content_cipher: self.content_cipher,
138 key_version: self.key_version,
139 plaintext_digest: self.plaintext_digest.to_string(),
140 plaintext_byte_len: self.plaintext_byte_len,
141 })
142 .map_err(|error| error.to_string())
143 }
144
145 pub fn decoded_nonce(&self) -> Result<[u8; AES_256_GCM_NONCE_BYTE_LEN], String> {
146 decode_exact(
147 &self.nonce,
148 AES_256_GCM_NONCE_BYTE_LEN,
149 "encrypted artifact nonce",
150 )?
151 .try_into()
152 .map_err(|_| "encrypted artifact nonce is invalid".to_string())
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase", deny_unknown_fields)]
158pub struct PackageKeyEnvelope {
159 pub schema_version: u32,
160 pub algorithm: KeyEnvelopeAlgorithm,
161 pub recipient_key_id: String,
162 pub recipient_key_fingerprint: ContentDigest,
163 pub ephemeral_public_key: String,
164 pub nonce: String,
165 pub wrapped_key: String,
166 pub context_digest: ContentDigest,
167}
168
169impl PackageKeyEnvelope {
170 pub fn validate(
171 &self,
172 recipient: &RecipientEncryptionKey,
173 artifact: &EncryptedArtifactMetadata,
174 ) -> Result<(), String> {
175 if self.schema_version != PACKAGE_KEY_ENVELOPE_SCHEMA_VERSION
176 || self.recipient_key_id != recipient.id
177 || self.recipient_key_fingerprint != recipient.fingerprint
178 {
179 return Err("package key envelope recipient is invalid".to_string());
180 }
181 recipient.validate()?;
182 let ephemeral_public_key = decode_exact(
183 &self.ephemeral_public_key,
184 P256_PUBLIC_KEY_BYTE_LEN,
185 "package key envelope ephemeral public key",
186 )?;
187 if p256::PublicKey::from_sec1_bytes(&ephemeral_public_key).is_err() {
188 return Err("package key envelope ephemeral public key is invalid".to_string());
189 }
190 decode_exact(
191 &self.nonce,
192 AES_256_GCM_NONCE_BYTE_LEN,
193 "package key envelope nonce",
194 )?;
195 decode_exact(
196 &self.wrapped_key,
197 AES_256_GCM_WRAPPED_KEY_BYTE_LEN,
198 "wrapped package content key",
199 )?;
200 if self.context_digest != ContentDigest::sha256(self.context_bytes(artifact)?) {
201 return Err("package key envelope context digest is invalid".to_string());
202 }
203 Ok(())
204 }
205
206 pub fn context_bytes(&self, artifact: &EncryptedArtifactMetadata) -> Result<Vec<u8>, String> {
207 #[derive(Serialize)]
208 #[serde(rename_all = "camelCase")]
209 struct Canonical<'a> {
210 format: &'static str,
211 artifact_aad_digest: String,
212 key_version: u64,
213 recipient_key_id: &'a str,
214 recipient_key_fingerprint: String,
215 ephemeral_public_key: &'a str,
216 algorithm: KeyEnvelopeAlgorithm,
217 }
218 if self.recipient_key_id.trim().is_empty() || self.recipient_key_id.len() > 128 {
219 return Err("package key envelope recipient is invalid".to_string());
220 }
221 serde_json::to_vec(&Canonical {
222 format: "runmat-package-key-envelope-context-v1",
223 artifact_aad_digest: artifact.aad_digest.to_string(),
224 key_version: artifact.key_version,
225 recipient_key_id: &self.recipient_key_id,
226 recipient_key_fingerprint: self.recipient_key_fingerprint.to_string(),
227 ephemeral_public_key: &self.ephemeral_public_key,
228 algorithm: self.algorithm,
229 })
230 .map_err(|error| error.to_string())
231 }
232
233 pub fn decoded_ephemeral_public_key(&self) -> Result<Vec<u8>, String> {
234 decode_exact(
235 &self.ephemeral_public_key,
236 P256_PUBLIC_KEY_BYTE_LEN,
237 "package key envelope ephemeral public key",
238 )
239 }
240
241 pub fn decoded_nonce(&self) -> Result<[u8; AES_256_GCM_NONCE_BYTE_LEN], String> {
242 decode_exact(
243 &self.nonce,
244 AES_256_GCM_NONCE_BYTE_LEN,
245 "package key envelope nonce",
246 )?
247 .try_into()
248 .map_err(|_| "package key envelope nonce is invalid".to_string())
249 }
250
251 pub fn decoded_wrapped_key(&self) -> Result<Vec<u8>, String> {
252 decode_exact(
253 &self.wrapped_key,
254 AES_256_GCM_WRAPPED_KEY_BYTE_LEN,
255 "wrapped package content key",
256 )
257 }
258}
259
260fn decode_exact(value: &str, expected: usize, label: &str) -> Result<Vec<u8>, String> {
261 let decoded = URL_SAFE_NO_PAD
262 .decode(value)
263 .map_err(|_| format!("{label} is not canonical base64url"))?;
264 if decoded.len() != expected || URL_SAFE_NO_PAD.encode(&decoded) != value {
265 return Err(format!("{label} has an invalid length or encoding"));
266 }
267 Ok(decoded)
268}