1use crate::app_action::{validate_app_action, APP_ACTION_PROFILE};
4use crate::*;
5use cose2::{iana, Header, Key, Label, Sign1Message, Value, Verifier};
6use dmsg_types::*;
7
8pub const TEXT_PROFILE: &str = "application/vnd.dmsg.text-statement+cose;v=1";
10pub const DIGEST_PROFILE: &str = "application/vnd.dmsg.digest-statement+cose;v=1";
12pub const FILE_STATEMENT_PROFILE: &str = "application/vnd.dmsg.file-statement+cose;v=1";
14pub const FILE_STATEMENT_CONTENT_TYPE: &str = "application/cbor";
16pub const MAX_FILE_STATEMENT_BYTES: usize = 16_384;
18pub const TEXT_CONTENT_TYPE: &str = "text/plain;charset=utf-8";
20pub const CWT_CLAIMS: i64 = 15;
22pub const TYPE_HEADER: i64 = 16;
24pub const HASH_ALGORITHM: i64 = 258;
26pub const PREIMAGE_CONTENT_TYPE: i64 = 259;
28pub const PAYLOAD_LOCATION: i64 = 260;
30pub const CTT_HEADER: i64 = 270;
32pub const MAX_KID_BYTES: usize = 256;
34pub const MAX_TIMESTAMP_BYTES: usize = 131_072;
36pub const MAX_COSE_KEY_BYTES: usize = 2048;
38pub const MAX_ARTIFACT_BYTES: usize = MAX_PAYLOAD + MAX_TIMESTAMP_BYTES;
40const CRITICAL: [Label; 3] = [
41 Label::Int(CWT_CLAIMS),
42 Label::Int(TYPE_HEADER),
43 Label::Int(HASH_ALGORITHM),
44];
45
46#[derive(cbor2::Cbor)]
49struct FileStatementPayload {
50 #[cbor(key = 1)]
51 text: String,
52 #[cbor(key = 2)]
53 sha256: Hash,
54 #[cbor(key = 3)]
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 content_type: Option<String>,
57 #[cbor(key = 4)]
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 location: Option<String>,
60}
61
62pub(crate) fn malformed(_: cose2::Error) -> Error {
63 Error::IntegrityFailed
64}
65
66pub const COSE_ALGORITHM: Label = Label::Int(iana::AlgorithmEd25519);
68
69pub fn statement_purpose(statement: &Statement) -> KeyPurpose {
73 match statement.content {
74 StatementContent::AppAction(_) => KeyPurpose::AppAction,
75 StatementContent::Text(_) | StatementContent::FileStatement { .. } => KeyPurpose::Statement,
76 StatementContent::Digest { .. } => KeyPurpose::FileAttestation,
77 }
78}
79
80fn media_type(value: &str) -> Result<()> {
81 ensure_valid(
82 value.len() <= 256 && value.parse::<mime::Mime>().is_ok(),
83 "media type",
84 )
85}
86
87pub fn validate_statement(statement: &Statement) -> Result<()> {
99 validate_uri(&statement.issuer)?;
100 if let StatementContent::AppAction(action) = &statement.content {
101 validate_app_action(action)?;
102 ensure(
104 statement.subject.is_none() && statement.issued_at.is_none(),
105 Error::UnsupportedProtocol,
106 )?;
107 }
108 if let Some(subject) = &statement.subject {
109 ensure_valid(
110 !subject.is_empty()
111 && subject.len() <= MAX_URI_BYTES
112 && !subject.chars().any(char::is_control),
113 "statement subject",
114 )?;
115 if subject.contains(':') {
116 validate_uri(subject)?;
117 }
118 }
119 if let StatementContent::Text(text) | StatementContent::FileStatement { text, .. } =
120 &statement.content
121 {
122 ensure(!text.is_empty() && text.len() <= 4096, Error::QuotaExceeded)?;
123 }
124 if let StatementContent::Digest {
125 content_type,
126 location,
127 ..
128 }
129 | StatementContent::FileStatement {
130 content_type,
131 location,
132 ..
133 } = &statement.content
134 {
135 if let Some(value) = content_type {
136 media_type(value)?;
137 }
138 if let Some(value) = location {
139 validate_uri(value)?;
140 }
141 }
142 Ok(())
143}
144
145fn parse_file_statement(payload: &[u8]) -> Result<StatementContent> {
146 ensure(
147 payload.len() <= MAX_FILE_STATEMENT_BYTES,
148 Error::QuotaExceeded,
149 )?;
150 let FileStatementPayload {
151 text,
152 sha256,
153 content_type,
154 location,
155 } = decode_canonical(payload)?;
156 Ok(StatementContent::FileStatement {
157 text,
158 sha256,
159 content_type,
160 location,
161 })
162}
163
164fn claims(statement: &Statement) -> Value {
165 let mut values = vec![(Value::from(1), Value::Text(statement.issuer.clone()))];
166 if let Some(subject) = &statement.subject {
167 values.push((Value::from(2), Value::Text(subject.clone())));
168 }
169 if let Some(at) = statement.issued_at {
170 values.push((Value::from(6), Value::from(at)));
171 }
172 Value::Map(values)
173}
174
175pub fn prepare_cose(statement: &Statement, kid: &[u8]) -> Result<(Sign1Message, Vec<u8>)> {
187 validate_statement(statement)?;
188 ensure_valid(!kid.is_empty() && kid.len() <= MAX_KID_BYTES, "kid")?;
189 let payload = match &statement.content {
190 StatementContent::AppAction(action) => canonical(action),
191 StatementContent::Text(text) => text.as_bytes().to_vec(),
192 StatementContent::Digest { sha256, .. } => sha256.to_vec(),
193 StatementContent::FileStatement {
194 text,
195 sha256,
196 content_type,
197 location,
198 } => canonical(&FileStatementPayload {
199 text: text.clone(),
200 sha256: *sha256,
201 content_type: content_type.clone(),
202 location: location.clone(),
203 }),
204 };
205 let mut message = Sign1Message::new(Some(payload));
206 message.protected.set_kid(kid.to_vec());
207 message.protected.insert(CWT_CLAIMS, claims(statement));
208 match &statement.content {
209 StatementContent::AppAction(_) => {
210 message.protected.set_crit([CWT_CLAIMS, TYPE_HEADER]);
211 message.protected.insert(TYPE_HEADER, APP_ACTION_PROFILE);
212 message.protected.set_content_type("application/cbor");
213 }
214 StatementContent::Text(_) => {
215 message.protected.set_crit([CWT_CLAIMS, TYPE_HEADER]);
216 message.protected.insert(TYPE_HEADER, TEXT_PROFILE);
217 message.protected.set_content_type(TEXT_CONTENT_TYPE);
218 }
219 StatementContent::FileStatement { .. } => {
220 message.protected.set_crit([CWT_CLAIMS, TYPE_HEADER]);
221 message
222 .protected
223 .insert(TYPE_HEADER, FILE_STATEMENT_PROFILE);
224 message
225 .protected
226 .set_content_type(FILE_STATEMENT_CONTENT_TYPE);
227 }
228 StatementContent::Digest {
229 content_type,
230 location,
231 ..
232 } => {
233 message
234 .protected
235 .set_crit([CWT_CLAIMS, TYPE_HEADER, HASH_ALGORITHM]);
236 message.protected.insert(TYPE_HEADER, DIGEST_PROFILE);
237 message
238 .protected
239 .insert(HASH_ALGORITHM, iana::AlgorithmSHA_256);
240 if let Some(value) = content_type {
241 message
242 .protected
243 .insert(PREIMAGE_CONTENT_TYPE, value.clone());
244 }
245 if let Some(value) = location {
246 message.protected.insert(PAYLOAD_LOCATION, value.clone());
247 }
248 }
249 }
250 let tbs = message
251 .prepare_signature(Some(COSE_ALGORITHM), None, None)
252 .map_err(malformed)?;
253 ensure(tbs.len() <= MAX_PAYLOAD, Error::QuotaExceeded)?;
254 Ok((message, tbs))
255}
256
257fn text(header: &Header, label: i64) -> Result<Option<&str>> {
258 match header.get(label) {
259 None => Ok(None),
260 Some(Value::Text(value)) => Ok(Some(value)),
261 _ => Err(Error::IntegrityFailed),
262 }
263}
264
265fn parse_claims(header: &Header) -> Result<(String, Option<String>, Option<i64>)> {
266 let Some(Value::Map(values)) = header.get(CWT_CLAIMS) else {
267 return Err(Error::IntegrityFailed);
268 };
269 let (mut issuer, mut subject, mut issued_at) = (None, None, None);
270 for (key, value) in values {
271 match (key, value) {
272 (Value::Integer(key), Value::Text(value)) if *key == 1.into() && issuer.is_none() => {
273 issuer = Some(value.clone())
274 }
275 (Value::Integer(key), Value::Text(value)) if *key == 2.into() && subject.is_none() => {
276 subject = Some(value.clone())
277 }
278 (Value::Integer(key), Value::Integer(value))
279 if *key == 6.into() && issued_at.is_none() =>
280 {
281 issued_at = Some(i64::try_from(*value).map_err(|_| Error::IntegrityFailed)?)
282 }
283 _ => return Err(Error::UnsupportedProtocol),
284 }
285 }
286 Ok((issuer.ok_or(Error::IntegrityFailed)?, subject, issued_at))
287}
288
289fn parse_message(message: &Sign1Message) -> Result<(Statement, &[u8])> {
290 let headers = &message.protected;
291 ensure(
292 headers.alg().map_err(malformed)?.as_ref() == Some(&COSE_ALGORITHM),
293 Error::UnsupportedProtocol,
294 )?;
295 let kid = headers
296 .kid()
297 .map_err(malformed)?
298 .ok_or(Error::IntegrityFailed)?;
299 ensure(
300 !kid.is_empty() && kid.len() <= MAX_KID_BYTES,
301 Error::IntegrityFailed,
302 )?;
303 ensure(
304 message.unprotected.iter().all(|(label, value)| {
305 *label == Label::Int(CTT_HEADER)
306 && matches!(value, Value::Bytes(b) if !b.is_empty() && b.len() <= MAX_TIMESTAMP_BYTES)
307 }),
308 Error::UnsupportedProtocol,
309 )?;
310 let payload = message.payload.as_deref().ok_or(Error::IntegrityFailed)?;
311 let profile = text(headers, TYPE_HEADER)?.ok_or(Error::IntegrityFailed)?;
312 let (allowed, critical, content): (&[i64], &[Label], StatementContent) = match profile {
313 APP_ACTION_PROFILE => {
314 ensure(
315 text(headers, iana::HeaderParameterContentType)? == Some("application/cbor"),
316 Error::UnsupportedProtocol,
317 )?;
318 (
319 &[1, 2, 3, 4, 15, 16],
320 &CRITICAL[..2],
321 StatementContent::AppAction(Box::new(decode_canonical(payload)?)),
322 )
323 }
324 TEXT_PROFILE => {
325 ensure(
326 text(headers, iana::HeaderParameterContentType)? == Some(TEXT_CONTENT_TYPE),
327 Error::UnsupportedProtocol,
328 )?;
329 (
330 &[1, 2, 3, 4, 15, 16],
331 &CRITICAL[..2],
332 StatementContent::Text(
333 std::str::from_utf8(payload)
334 .map_err(|_| Error::IntegrityFailed)?
335 .into(),
336 ),
337 )
338 }
339 FILE_STATEMENT_PROFILE => {
340 ensure(
341 text(headers, iana::HeaderParameterContentType)?
342 == Some(FILE_STATEMENT_CONTENT_TYPE),
343 Error::UnsupportedProtocol,
344 )?;
345 (
346 &[1, 2, 3, 4, 15, 16],
347 &CRITICAL[..2],
348 parse_file_statement(payload)?,
349 )
350 }
351 DIGEST_PROFILE => {
352 ensure(
353 headers.get_i64(HASH_ALGORITHM).map_err(malformed)? == Some(iana::AlgorithmSHA_256),
354 Error::UnsupportedProtocol,
355 )?;
356 (
357 &[1, 2, 4, 15, 16, 258, 259, 260],
358 &CRITICAL,
359 StatementContent::Digest {
360 sha256: Hash::new(payload.try_into().map_err(|_| Error::IntegrityFailed)?),
361 content_type: text(headers, PREIMAGE_CONTENT_TYPE)?.map(str::to_owned),
362 location: text(headers, PAYLOAD_LOCATION)?.map(str::to_owned),
363 },
364 )
365 }
366 _ => return Err(Error::UnsupportedProtocol),
367 };
368 ensure(
369 headers
370 .iter()
371 .all(|(label, _)| matches!(label, Label::Int(id) if allowed.contains(id))),
372 Error::UnsupportedProtocol,
373 )?;
374 let declared = headers
375 .crit()
376 .map_err(malformed)?
377 .ok_or(Error::IntegrityFailed)?;
378 ensure(
379 declared.len() == critical.len() && critical.iter().all(|label| declared.contains(label)),
380 Error::UnsupportedProtocol,
381 )?;
382 headers
383 .ensure_crit_understood(&CRITICAL)
384 .map_err(malformed)?;
385 let (issuer, subject, issued_at) = parse_claims(headers)?;
386 let statement = Statement {
387 issuer,
388 subject,
389 issued_at,
390 content,
391 };
392 validate_statement(&statement)?;
393 Ok((statement, kid))
394}
395
396pub fn public_cose_key(kid: &[u8], public: &[u8]) -> Result<Vec<u8>> {
407 ensure_valid(kid.len() <= MAX_KID_BYTES, "kid")?;
408 validate_ed25519_key(public)?;
409 let mut key = Key::new();
410 key.set_alg(COSE_ALGORITHM).set_kid(kid.to_vec());
411 key.set_ops([iana::KeyOperationVerify]);
412 key.set_kty(iana::KeyTypeOKP);
413 key.insert(iana::OKPKeyParameterCrv, iana::EllipticCurveEd25519);
414 key.insert(iana::OKPKeyParameterX, public.to_vec());
415 key.to_vec().map_err(malformed)
416}
417
418pub fn key_thumbprint(encoded: &[u8]) -> Result<Hash> {
427 ensure(encoded.len() <= MAX_COSE_KEY_BYTES, Error::QuotaExceeded)?;
428 let key = Key::from_slice(encoded).map_err(malformed)?;
429 thumbprint(&key)
430}
431
432pub(crate) fn thumbprint(key: &Key) -> Result<Hash> {
433 ensure(
434 !key.contains_key(iana::OKPKeyParameterD),
435 Error::IntegrityFailed,
436 )?;
437 ensure(
438 key.kty().map_err(malformed)? == Some(Label::Int(iana::KeyTypeOKP)),
439 Error::UnsupportedProtocol,
440 )?;
441 let mut required = Key::new();
442 for label in [1, -1, -2] {
443 required.insert(
444 label,
445 key.get(label).ok_or(Error::IntegrityFailed)?.clone(),
446 );
447 }
448 Ok(sha256(&required.to_vec().map_err(malformed)?))
449}
450
451struct ProfileVerifier(cose2::ed25519::Ed25519Verifier);
452
453impl Verifier for ProfileVerifier {
454 fn alg(&self) -> Option<Label> {
455 Some(COSE_ALGORITHM)
456 }
457
458 fn understood_critical_headers(&self) -> &[Label] {
459 &CRITICAL
460 }
461
462 fn verify(&self, bytes: &[u8], signature: &[u8]) -> std::result::Result<(), cose2::Error> {
463 self.0.verify(bytes, signature)
464 }
465}
466
467fn verifier(key: &Key) -> Result<ProfileVerifier> {
468 ensure(
469 !key.contains_key(iana::OKPKeyParameterD)
470 && key
471 .allows_any_operation(&[iana::KeyOperationVerify])
472 .map_err(malformed)?,
473 Error::IntegrityFailed,
474 )?;
475 ensure(
476 key.alg().map_err(malformed)? == Some(COSE_ALGORITHM),
477 Error::IntegrityFailed,
478 )?;
479 Ok(ProfileVerifier(
480 cose2::ed25519::Ed25519Verifier::from_cose_key(key).map_err(malformed)?,
481 ))
482}
483
484pub fn verify_artifact(artifact: &SignedArtifact) -> Result<Statement> {
497 Ok(verify_and_parse_artifact(artifact)?.statement)
498}
499
500pub(crate) struct VerifiedArtifact {
502 pub statement: Statement,
503 pub message: Sign1Message,
504 pub key: Key,
505}
506
507impl VerifiedArtifact {
508 pub fn signing_bytes(&self) -> Result<Vec<u8>> {
509 Sign1Message::to_be_signed(
510 self.message.protected_raw(),
511 &[],
512 self.message
513 .payload
514 .as_deref()
515 .ok_or(Error::IntegrityFailed)?,
516 )
517 .map_err(malformed)
518 }
519}
520
521pub(crate) fn verify_and_parse_artifact(artifact: &SignedArtifact) -> Result<VerifiedArtifact> {
522 ensure(
523 artifact.cose_sign1.len() <= MAX_ARTIFACT_BYTES
524 && artifact.cose_key.len() <= MAX_COSE_KEY_BYTES,
525 Error::QuotaExceeded,
526 )?;
527 ensure(
528 artifact.cose_sign1.first() == Some(&0xd2),
529 Error::IntegrityFailed,
530 )?;
531 let message = Sign1Message::from_slice(&artifact.cose_sign1).map_err(malformed)?;
532 let (statement, kid) = parse_message(&message)?;
533 let key = Key::from_slice(&artifact.cose_key).map_err(malformed)?;
534 ensure(
535 key.kid().map_err(malformed)?.is_none_or(|id| id == kid),
536 Error::IntegrityFailed,
537 )?;
538 message.verify(&verifier(&key)?, None).map_err(malformed)?;
539 Ok(VerifiedArtifact {
540 statement,
541 message,
542 key,
543 })
544}
545
546pub fn signature_digest(cose_sign1: &[u8]) -> Result<Hash> {
556 ensure(cose_sign1.len() <= MAX_ARTIFACT_BYTES, Error::QuotaExceeded)?;
557 Ok(sha256(
558 Sign1Message::from_slice(cose_sign1)
559 .map_err(malformed)?
560 .signature(),
561 ))
562}