ndn_cert/
tlv.rs

1//! NDNCERT 0.3 TLV wire format.
2//!
3//! Replaces the Phase 1A/1B JSON encoding with full NDN TLV for complete
4//! interoperability with the reference C++ implementation
5//! (`ndncert-ca-server` / `ndncert-client`).
6//!
7//! # Type assignments (NDNCERT 0.3)
8//!
9//! ```text
10//! ca-prefix          0x81 (129)
11//! ca-info            0x83 (131)
12//! parameter-key      0x85 (133)
13//! parameter-value    0x87 (135)
14//! ca-certificate     0x89 (137)
15//! max-validity       0x8B (139)
16//! probe-response     0x8D (141)
17//! max-suffix-length  0x8F (143)
18//! ecdh-pub           0x91 (145)
19//! cert-request       0x93 (147)
20//! salt               0x95 (149)
21//! request-id         0x97 (151)
22//! challenge          0x99 (153)
23//! status             0x9B (155)
24//! iv                 0x9D (157)
25//! encrypted-payload  0x9F (159)
26//! selected-challenge 0xA1 (161)
27//! challenge-status   0xA3 (163)
28//! remaining-tries    0xA5 (165)
29//! remaining-time     0xA7 (167)
30//! issued-cert-name   0xA9 (169)
31//! error-code         0xAB (171)
32//! error-info         0xAD (173)
33//! auth-tag           0xAF (175)
34//! ```
35
36use bytes::Bytes;
37use ndn_tlv::{TlvReader, TlvWriter};
38
39use crate::error::CertError;
40
41// ── TLV type constants ────────────────────────────────────────────────────────
42
43pub const TLV_CA_PREFIX: u64 = 0x81;
44pub const TLV_CA_INFO: u64 = 0x83;
45pub const TLV_PARAMETER_KEY: u64 = 0x85;
46pub const TLV_PARAMETER_VALUE: u64 = 0x87;
47pub const TLV_CA_CERTIFICATE: u64 = 0x89;
48pub const TLV_MAX_VALIDITY: u64 = 0x8B;
49pub const TLV_PROBE_RESPONSE: u64 = 0x8D;
50pub const TLV_MAX_SUFFIX_LENGTH: u64 = 0x8F;
51pub const TLV_ECDH_PUB: u64 = 0x91;
52pub const TLV_CERT_REQUEST: u64 = 0x93;
53pub const TLV_SALT: u64 = 0x95;
54pub const TLV_REQUEST_ID: u64 = 0x97;
55pub const TLV_CHALLENGE: u64 = 0x99;
56pub const TLV_STATUS: u64 = 0x9B;
57pub const TLV_IV: u64 = 0x9D;
58pub const TLV_ENCRYPTED_PAYLOAD: u64 = 0x9F;
59pub const TLV_SELECTED_CHALLENGE: u64 = 0xA1;
60pub const TLV_CHALLENGE_STATUS: u64 = 0xA3;
61pub const TLV_REMAINING_TRIES: u64 = 0xA5;
62pub const TLV_REMAINING_TIME: u64 = 0xA7;
63pub const TLV_ISSUED_CERT_NAME: u64 = 0xA9;
64pub const TLV_ERROR_CODE: u64 = 0xAB;
65pub const TLV_ERROR_INFO: u64 = 0xAD;
66pub const TLV_AUTH_TAG: u64 = 0xAF;
67
68// ── TLV-encoded protocol messages ─────────────────────────────────────────────
69
70/// TLV-encoded CA profile (content of `/<ca>/CA/INFO` Data packet).
71pub struct CaProfileTlv {
72    pub ca_prefix: String,
73    pub ca_info: String,
74    pub ca_certificate: Bytes,
75    pub max_validity_secs: u64,
76    pub challenges: Vec<String>,
77}
78
79impl CaProfileTlv {
80    pub fn encode(&self) -> Bytes {
81        let mut w = TlvWriter::new();
82        w.write_tlv(TLV_CA_PREFIX, self.ca_prefix.as_bytes());
83        w.write_tlv(TLV_CA_INFO, self.ca_info.as_bytes());
84        w.write_tlv(TLV_CA_CERTIFICATE, &self.ca_certificate);
85        w.write_tlv(TLV_MAX_VALIDITY, &self.max_validity_secs.to_be_bytes());
86        for challenge in &self.challenges {
87            w.write_tlv(TLV_CHALLENGE, challenge.as_bytes());
88        }
89        w.finish()
90    }
91
92    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
93        let mut r = TlvReader::new(buf);
94        let mut ca_prefix = None;
95        let mut ca_info = None;
96        let mut ca_certificate = Bytes::new();
97        let mut max_validity_secs = 86400u64;
98        let mut challenges = Vec::new();
99
100        while !r.is_empty() {
101            let (typ, val) = r
102                .read_tlv()
103                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
104            match typ {
105                TLV_CA_PREFIX => {
106                    ca_prefix = Some(
107                        std::str::from_utf8(&val)
108                            .map_err(|_| {
109                                CertError::InvalidRequest("invalid ca-prefix UTF-8".into())
110                            })?
111                            .to_string(),
112                    );
113                }
114                TLV_CA_INFO => {
115                    ca_info = Some(
116                        std::str::from_utf8(&val)
117                            .map_err(|_| CertError::InvalidRequest("invalid ca-info UTF-8".into()))?
118                            .to_string(),
119                    );
120                }
121                TLV_CA_CERTIFICATE => {
122                    ca_certificate = val;
123                }
124                TLV_MAX_VALIDITY => {
125                    if val.len() >= 8 {
126                        max_validity_secs = u64::from_be_bytes(val[..8].try_into().unwrap());
127                    }
128                }
129                TLV_CHALLENGE => {
130                    let s = std::str::from_utf8(&val)
131                        .map_err(|_| CertError::InvalidRequest("invalid challenge UTF-8".into()))?
132                        .to_string();
133                    challenges.push(s);
134                }
135                _ => {} // unknown TLV — skip (forward compatibility)
136            }
137        }
138
139        Ok(Self {
140            ca_prefix: ca_prefix
141                .ok_or_else(|| CertError::InvalidRequest("missing ca-prefix".into()))?,
142            ca_info: ca_info.unwrap_or_default(),
143            ca_certificate,
144            max_validity_secs,
145            challenges,
146        })
147    }
148}
149
150/// TLV-encoded NEW request (ApplicationParameters of `/<ca>/CA/NEW`).
151///
152/// Per NDNCERT 0.3 the request carries an ECDH ephemeral public key (65 bytes,
153/// uncompressed P-256 point) instead of the raw Ed25519 public key from Phase 1A.
154/// The cert request bytes (name + not-before + not-after + Ed25519 pubkey) are
155/// nested under `TLV_CERT_REQUEST`.
156pub struct NewRequestTlv {
157    /// Uncompressed P-256 ephemeral public key (65 bytes).
158    pub ecdh_pub: Bytes,
159    /// DER/TLV-encoded self-signed certificate of the requester.
160    pub cert_request: Bytes,
161}
162
163impl NewRequestTlv {
164    pub fn encode(&self) -> Bytes {
165        let mut w = TlvWriter::new();
166        w.write_tlv(TLV_ECDH_PUB, &self.ecdh_pub);
167        w.write_tlv(TLV_CERT_REQUEST, &self.cert_request);
168        w.finish()
169    }
170
171    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
172        let mut r = TlvReader::new(buf);
173        let mut ecdh_pub = None;
174        let mut cert_request = None;
175
176        while !r.is_empty() {
177            let (typ, val) = r
178                .read_tlv()
179                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
180            match typ {
181                TLV_ECDH_PUB => ecdh_pub = Some(val),
182                TLV_CERT_REQUEST => cert_request = Some(val),
183                _ => {}
184            }
185        }
186
187        Ok(Self {
188            ecdh_pub: ecdh_pub
189                .ok_or_else(|| CertError::InvalidRequest("missing ecdh-pub".into()))?,
190            cert_request: cert_request
191                .ok_or_else(|| CertError::InvalidRequest("missing cert-request".into()))?,
192        })
193    }
194}
195
196/// TLV-encoded NEW response.
197pub struct NewResponseTlv {
198    /// CA's ECDH ephemeral public key (65 bytes).
199    pub ecdh_pub: Bytes,
200    /// Random 32-byte salt for HKDF.
201    pub salt: [u8; 32],
202    /// 8-byte request identifier.
203    pub request_id: [u8; 8],
204    /// Supported challenge type names.
205    pub challenges: Vec<String>,
206}
207
208impl NewResponseTlv {
209    pub fn encode(&self) -> Bytes {
210        let mut w = TlvWriter::new();
211        w.write_tlv(TLV_ECDH_PUB, &self.ecdh_pub);
212        w.write_tlv(TLV_SALT, &self.salt);
213        w.write_tlv(TLV_REQUEST_ID, &self.request_id);
214        for challenge in &self.challenges {
215            w.write_tlv(TLV_CHALLENGE, challenge.as_bytes());
216        }
217        w.finish()
218    }
219
220    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
221        let mut r = TlvReader::new(buf);
222        let mut ecdh_pub = None;
223        let mut salt = None;
224        let mut request_id = None;
225        let mut challenges = Vec::new();
226
227        while !r.is_empty() {
228            let (typ, val) = r
229                .read_tlv()
230                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
231            match typ {
232                TLV_ECDH_PUB => ecdh_pub = Some(val),
233                TLV_SALT => {
234                    if val.len() == 32 {
235                        let mut arr = [0u8; 32];
236                        arr.copy_from_slice(&val);
237                        salt = Some(arr);
238                    }
239                }
240                TLV_REQUEST_ID => {
241                    if val.len() == 8 {
242                        let mut arr = [0u8; 8];
243                        arr.copy_from_slice(&val);
244                        request_id = Some(arr);
245                    }
246                }
247                TLV_CHALLENGE => {
248                    if let Ok(s) = std::str::from_utf8(&val) {
249                        challenges.push(s.to_string());
250                    }
251                }
252                _ => {}
253            }
254        }
255
256        Ok(Self {
257            ecdh_pub: ecdh_pub
258                .ok_or_else(|| CertError::InvalidRequest("missing ecdh-pub".into()))?,
259            salt: salt.ok_or_else(|| CertError::InvalidRequest("missing salt".into()))?,
260            request_id: request_id
261                .ok_or_else(|| CertError::InvalidRequest("missing request-id".into()))?,
262            challenges,
263        })
264    }
265}
266
267/// TLV-encoded CHALLENGE request (encrypted).
268///
269/// The `parameters` field from Phase 1B is replaced by an AES-GCM-128
270/// ciphertext. The plaintext is the JSON-encoded parameters map.
271pub struct ChallengeRequestTlv {
272    /// 8-byte request identifier (must match NewResponse).
273    pub request_id: [u8; 8],
274    /// Selected challenge type name.
275    pub selected_challenge: String,
276    /// 12-byte AES-GCM initialization vector.
277    pub iv: [u8; 12],
278    /// AES-GCM-128 ciphertext of the JSON parameters.
279    pub encrypted_payload: Bytes,
280    /// 16-byte AES-GCM authentication tag.
281    pub auth_tag: [u8; 16],
282}
283
284impl ChallengeRequestTlv {
285    pub fn encode(&self) -> Bytes {
286        let mut w = TlvWriter::new();
287        w.write_tlv(TLV_REQUEST_ID, &self.request_id);
288        w.write_tlv(TLV_SELECTED_CHALLENGE, self.selected_challenge.as_bytes());
289        w.write_tlv(TLV_IV, &self.iv);
290        w.write_tlv(TLV_ENCRYPTED_PAYLOAD, &self.encrypted_payload);
291        w.write_tlv(TLV_AUTH_TAG, &self.auth_tag);
292        w.finish()
293    }
294
295    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
296        let mut r = TlvReader::new(buf);
297        let mut request_id = None;
298        let mut selected_challenge = None;
299        let mut iv = None;
300        let mut encrypted_payload = None;
301        let mut auth_tag = None;
302
303        while !r.is_empty() {
304            let (typ, val) = r
305                .read_tlv()
306                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
307            match typ {
308                TLV_REQUEST_ID => {
309                    if val.len() == 8 {
310                        let mut arr = [0u8; 8];
311                        arr.copy_from_slice(&val);
312                        request_id = Some(arr);
313                    }
314                }
315                TLV_SELECTED_CHALLENGE => {
316                    selected_challenge = Some(
317                        std::str::from_utf8(&val)
318                            .map_err(|_| {
319                                CertError::InvalidRequest("invalid challenge type UTF-8".into())
320                            })?
321                            .to_string(),
322                    );
323                }
324                TLV_IV => {
325                    if val.len() == 12 {
326                        let mut arr = [0u8; 12];
327                        arr.copy_from_slice(&val);
328                        iv = Some(arr);
329                    }
330                }
331                TLV_ENCRYPTED_PAYLOAD => encrypted_payload = Some(val),
332                TLV_AUTH_TAG => {
333                    if val.len() == 16 {
334                        let mut arr = [0u8; 16];
335                        arr.copy_from_slice(&val);
336                        auth_tag = Some(arr);
337                    }
338                }
339                _ => {}
340            }
341        }
342
343        Ok(Self {
344            request_id: request_id
345                .ok_or_else(|| CertError::InvalidRequest("missing request-id".into()))?,
346            selected_challenge: selected_challenge
347                .ok_or_else(|| CertError::InvalidRequest("missing selected-challenge".into()))?,
348            iv: iv.ok_or_else(|| CertError::InvalidRequest("missing iv".into()))?,
349            encrypted_payload: encrypted_payload
350                .ok_or_else(|| CertError::InvalidRequest("missing encrypted-payload".into()))?,
351            auth_tag: auth_tag
352                .ok_or_else(|| CertError::InvalidRequest("missing auth-tag".into()))?,
353        })
354    }
355}
356
357/// TLV-encoded CHALLENGE response.
358pub struct ChallengeResponseTlv {
359    /// Numeric status code per NDNCERT 0.3 §3.3.
360    pub status: u8,
361    // Fields present on Processing status:
362    pub challenge_status: Option<String>,
363    pub remaining_tries: Option<u8>,
364    pub remaining_time_secs: Option<u32>,
365    // Fields present on Approved status:
366    pub issued_cert_name: Option<String>,
367    // Fields present on Denied status:
368    pub error_code: Option<u8>,
369    pub error_info: Option<String>,
370    // Encrypted payload for processing status (challenge data):
371    pub iv: Option<[u8; 12]>,
372    pub encrypted_payload: Option<Bytes>,
373    pub auth_tag: Option<[u8; 16]>,
374}
375
376/// Status codes per NDNCERT 0.3.
377pub const STATUS_BEFORE_CHALLENGE: u8 = 0;
378pub const STATUS_CHALLENGE: u8 = 1;
379pub const STATUS_PENDING: u8 = 2;
380pub const STATUS_SUCCESS: u8 = 3;
381pub const STATUS_FAILURE: u8 = 4;
382
383impl ChallengeResponseTlv {
384    pub fn encode(&self) -> Bytes {
385        let mut w = TlvWriter::new();
386        w.write_tlv(TLV_STATUS, &[self.status]);
387
388        if let Some(ref cs) = self.challenge_status {
389            w.write_tlv(TLV_CHALLENGE_STATUS, cs.as_bytes());
390        }
391        if let Some(rt) = self.remaining_tries {
392            w.write_tlv(TLV_REMAINING_TRIES, &[rt]);
393        }
394        if let Some(rt) = self.remaining_time_secs {
395            w.write_tlv(TLV_REMAINING_TIME, &rt.to_be_bytes());
396        }
397        if let Some(ref cn) = self.issued_cert_name {
398            w.write_tlv(TLV_ISSUED_CERT_NAME, cn.as_bytes());
399        }
400        if let Some(ec) = self.error_code {
401            w.write_tlv(TLV_ERROR_CODE, &[ec]);
402        }
403        if let Some(ref ei) = self.error_info {
404            w.write_tlv(TLV_ERROR_INFO, ei.as_bytes());
405        }
406        if let Some(ref iv) = self.iv {
407            w.write_tlv(TLV_IV, iv);
408        }
409        if let Some(ref ep) = self.encrypted_payload {
410            w.write_tlv(TLV_ENCRYPTED_PAYLOAD, ep);
411        }
412        if let Some(ref at) = self.auth_tag {
413            w.write_tlv(TLV_AUTH_TAG, at);
414        }
415        w.finish()
416    }
417
418    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
419        let mut r = TlvReader::new(buf);
420        let mut status = None;
421        let mut challenge_status = None;
422        let mut remaining_tries = None;
423        let mut remaining_time_secs = None;
424        let mut issued_cert_name = None;
425        let mut error_code = None;
426        let mut error_info = None;
427        let mut iv = None;
428        let mut encrypted_payload = None;
429        let mut auth_tag = None;
430
431        while !r.is_empty() {
432            let (typ, val) = r
433                .read_tlv()
434                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
435            match typ {
436                TLV_STATUS => {
437                    status = val.first().copied();
438                }
439                TLV_CHALLENGE_STATUS => {
440                    challenge_status = std::str::from_utf8(&val).ok().map(str::to_string);
441                }
442                TLV_REMAINING_TRIES => {
443                    remaining_tries = val.first().copied();
444                }
445                TLV_REMAINING_TIME => {
446                    if val.len() >= 4 {
447                        remaining_time_secs =
448                            Some(u32::from_be_bytes(val[..4].try_into().unwrap()));
449                    }
450                }
451                TLV_ISSUED_CERT_NAME => {
452                    issued_cert_name = std::str::from_utf8(&val).ok().map(str::to_string);
453                }
454                TLV_ERROR_CODE => {
455                    error_code = val.first().copied();
456                }
457                TLV_ERROR_INFO => {
458                    error_info = std::str::from_utf8(&val).ok().map(str::to_string);
459                }
460                TLV_IV => {
461                    if val.len() == 12 {
462                        let mut arr = [0u8; 12];
463                        arr.copy_from_slice(&val);
464                        iv = Some(arr);
465                    }
466                }
467                TLV_ENCRYPTED_PAYLOAD => encrypted_payload = Some(val),
468                TLV_AUTH_TAG => {
469                    if val.len() == 16 {
470                        let mut arr = [0u8; 16];
471                        arr.copy_from_slice(&val);
472                        auth_tag = Some(arr);
473                    }
474                }
475                _ => {}
476            }
477        }
478
479        Ok(Self {
480            status: status.ok_or_else(|| CertError::InvalidRequest("missing status".into()))?,
481            challenge_status,
482            remaining_tries,
483            remaining_time_secs,
484            issued_cert_name,
485            error_code,
486            error_info,
487            iv,
488            encrypted_payload,
489            auth_tag,
490        })
491    }
492}
493
494/// TLV-encoded PROBE response (content of `/<ca>/CA/PROBE` Data packet).
495pub struct ProbeResponseTlv {
496    /// Whether the CA will issue for the requested name.
497    pub allowed: bool,
498    /// Reason for denial (present when `allowed == false`).
499    pub reason: Option<String>,
500    /// Max components the CA permits after its own prefix.
501    pub max_suffix_length: Option<u8>,
502}
503
504impl ProbeResponseTlv {
505    pub fn encode(&self) -> Bytes {
506        let mut w = TlvWriter::new();
507        w.write_tlv(TLV_STATUS, &[if self.allowed { 1u8 } else { 0u8 }]);
508        if let Some(ref reason) = self.reason {
509            w.write_tlv(TLV_ERROR_INFO, reason.as_bytes());
510        }
511        if let Some(msl) = self.max_suffix_length {
512            w.write_tlv(TLV_MAX_SUFFIX_LENGTH, &[msl]);
513        }
514        w.finish()
515    }
516
517    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
518        let mut r = TlvReader::new(buf);
519        let mut allowed = None;
520        let mut reason = None;
521        let mut max_suffix_length = None;
522
523        while !r.is_empty() {
524            let (typ, val) = r
525                .read_tlv()
526                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
527            match typ {
528                TLV_STATUS => {
529                    allowed = val.first().map(|&b| b != 0);
530                }
531                TLV_ERROR_INFO => {
532                    reason = std::str::from_utf8(&val).ok().map(str::to_string);
533                }
534                TLV_MAX_SUFFIX_LENGTH => {
535                    max_suffix_length = val.first().copied();
536                }
537                _ => {}
538            }
539        }
540
541        Ok(Self {
542            allowed: allowed.unwrap_or(false),
543            reason,
544            max_suffix_length,
545        })
546    }
547}
548
549/// TLV-encoded REVOKE request body (`/<ca>/CA/REVOKE` ApplicationParameters).
550pub struct RevokeRequestTlv {
551    /// Name of the certificate to revoke (UTF-8 NDN name URI).
552    pub cert_name: String,
553    /// Ed25519 signature of `cert_name` bytes, proving possession.
554    pub signature: Bytes,
555}
556
557/// REVOKE response status codes.
558pub const REVOKE_STATUS_REVOKED: u8 = 0;
559pub const REVOKE_STATUS_NOT_FOUND: u8 = 1;
560pub const REVOKE_STATUS_UNAUTHORIZED: u8 = 2;
561
562impl RevokeRequestTlv {
563    pub fn encode(&self) -> Bytes {
564        let mut w = TlvWriter::new();
565        w.write_tlv(TLV_ISSUED_CERT_NAME, self.cert_name.as_bytes());
566        w.write_tlv(TLV_AUTH_TAG, &self.signature);
567        w.finish()
568    }
569
570    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
571        let mut r = TlvReader::new(buf);
572        let mut cert_name = None;
573        let mut signature = None;
574
575        while !r.is_empty() {
576            let (typ, val) = r
577                .read_tlv()
578                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
579            match typ {
580                TLV_ISSUED_CERT_NAME => {
581                    cert_name = Some(
582                        std::str::from_utf8(&val)
583                            .map_err(|_| {
584                                CertError::InvalidRequest("invalid cert-name UTF-8".into())
585                            })?
586                            .to_string(),
587                    );
588                }
589                TLV_AUTH_TAG => {
590                    signature = Some(val);
591                }
592                _ => {}
593            }
594        }
595
596        Ok(Self {
597            cert_name: cert_name
598                .ok_or_else(|| CertError::InvalidRequest("missing cert-name".into()))?,
599            signature: signature
600                .ok_or_else(|| CertError::InvalidRequest("missing signature".into()))?,
601        })
602    }
603}
604
605/// TLV-encoded REVOKE response body.
606pub struct RevokeResponseTlv {
607    /// One of the `REVOKE_STATUS_*` constants.
608    pub status: u8,
609    /// Optional human-readable reason.
610    pub reason: Option<String>,
611}
612
613impl RevokeResponseTlv {
614    pub fn encode(&self) -> Bytes {
615        let mut w = TlvWriter::new();
616        w.write_tlv(TLV_STATUS, &[self.status]);
617        if let Some(ref reason) = self.reason {
618            w.write_tlv(TLV_ERROR_INFO, reason.as_bytes());
619        }
620        w.finish()
621    }
622
623    pub fn decode(buf: Bytes) -> Result<Self, CertError> {
624        let mut r = TlvReader::new(buf);
625        let mut status = None;
626        let mut reason = None;
627
628        while !r.is_empty() {
629            let (typ, val) = r
630                .read_tlv()
631                .map_err(|e| CertError::InvalidRequest(format!("TLV parse error: {e}")))?;
632            match typ {
633                TLV_STATUS => {
634                    status = val.first().copied();
635                }
636                TLV_ERROR_INFO => {
637                    reason = std::str::from_utf8(&val).ok().map(str::to_string);
638                }
639                _ => {}
640            }
641        }
642
643        Ok(Self {
644            status: status.ok_or_else(|| CertError::InvalidRequest("missing status".into()))?,
645            reason,
646        })
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    #[test]
655    fn ca_profile_tlv_roundtrip() {
656        let profile = CaProfileTlv {
657            ca_prefix: "/com/acme/CA".to_string(),
658            ca_info: "ACME CA".to_string(),
659            ca_certificate: Bytes::from_static(b"\x01\x02\x03"),
660            max_validity_secs: 86400,
661            challenges: vec!["pin".to_string(), "email".to_string()],
662        };
663        let encoded = profile.encode();
664        let decoded = CaProfileTlv::decode(encoded).unwrap();
665        assert_eq!(decoded.ca_prefix, "/com/acme/CA");
666        assert_eq!(decoded.ca_info, "ACME CA");
667        assert_eq!(decoded.max_validity_secs, 86400);
668        assert_eq!(decoded.challenges, vec!["pin", "email"]);
669    }
670
671    #[test]
672    fn new_request_tlv_roundtrip() {
673        let req = NewRequestTlv {
674            ecdh_pub: Bytes::from(vec![0x04u8; 65]),
675            cert_request: Bytes::from_static(b"cert-data"),
676        };
677        let encoded = req.encode();
678        let decoded = NewRequestTlv::decode(encoded).unwrap();
679        assert_eq!(decoded.ecdh_pub.len(), 65);
680        assert_eq!(&decoded.cert_request[..], b"cert-data");
681    }
682
683    #[test]
684    fn new_response_tlv_roundtrip() {
685        let resp = NewResponseTlv {
686            ecdh_pub: Bytes::from(vec![0x04u8; 65]),
687            salt: [0xABu8; 32],
688            request_id: [0x01u8; 8],
689            challenges: vec!["possession".to_string()],
690        };
691        let encoded = resp.encode();
692        let decoded = NewResponseTlv::decode(encoded).unwrap();
693        assert_eq!(decoded.salt, [0xABu8; 32]);
694        assert_eq!(decoded.request_id, [0x01u8; 8]);
695        assert_eq!(decoded.challenges, vec!["possession"]);
696    }
697
698    #[test]
699    fn challenge_response_tlv_success_roundtrip() {
700        let resp = ChallengeResponseTlv {
701            status: STATUS_SUCCESS,
702            challenge_status: None,
703            remaining_tries: None,
704            remaining_time_secs: None,
705            issued_cert_name: Some("/com/acme/alice/KEY/v=0".to_string()),
706            error_code: None,
707            error_info: None,
708            iv: None,
709            encrypted_payload: None,
710            auth_tag: None,
711        };
712        let encoded = resp.encode();
713        let decoded = ChallengeResponseTlv::decode(encoded).unwrap();
714        assert_eq!(decoded.status, STATUS_SUCCESS);
715        assert_eq!(
716            decoded.issued_cert_name.as_deref(),
717            Some("/com/acme/alice/KEY/v=0")
718        );
719    }
720}