Skip to main content

zlicenser_protocol/evidence/
bundle.rs

1// Field order is the wire format. Do not reorder without a protocol version bump.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    crypto::signature::{Signature, SigningKey, VerifyingKey},
7    wire,
8};
9
10/// Legally-admissible record of a completed license exchange.
11/// Both sides hold a copy; neither can alter it without breaking both signatures.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct EvidenceBundle {
14    pub payload: EvidenceBundlePayload,
15    #[serde(with = "crate::wire::bytes::sig_bytes")]
16    pub vendor_signature: [u8; 64],
17    #[serde(with = "crate::wire::bytes::sig_bytes")]
18    pub customer_signature: [u8; 64],
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct EvidenceBundlePayload {
23    pub protocol_version: u16,
24    pub bundle_id: [u8; 16],
25
26    // all four protocol messages (raw CBOR bytes)
27    pub license_request: Vec<u8>,
28    pub license_grant: Vec<u8>,
29    pub receipt: Vec<u8>,
30    pub binding_certificate: Vec<u8>,
31
32    // terms and consent
33    /// hash of the exact terms text shown at purchase, vendor must produce the original to verify
34    pub terms_hash: [u8; 32],
35    pub consent: ConsentRecord,
36
37    // payment and timestamp
38    pub payment_reference: String,
39    pub tsa_token: Vec<u8>,
40
41    pub vendor_public_key: [u8; 32],
42    pub customer_public_key: [u8; 32],
43}
44
45/// Customer consent captured at purchase time.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct ConsentRecord {
48    pub checkboxes_ticked: Vec<String>,
49    pub consented_at: u64,
50    pub ip_address: String,
51}
52
53impl EvidenceBundle {
54    /// Canonical CBOR bytes of the payload, what both signatures are computed over.
55    pub fn payload_bytes(payload: &EvidenceBundlePayload) -> crate::Result<Vec<u8>> {
56        wire::encode(payload)
57    }
58
59    /// Vendor-signs the payload. Customer still needs to call `add_customer_signature`.
60    pub fn sign_vendor(
61        payload: EvidenceBundlePayload,
62        vendor_key: &SigningKey,
63    ) -> crate::Result<PartialBundle> {
64        let payload_bytes = Self::payload_bytes(&payload)?;
65        let sig = vendor_key.sign(&payload_bytes);
66        Ok(PartialBundle {
67            payload,
68            vendor_signature: sig.to_bytes(),
69        })
70    }
71
72    /// Verifies both signatures.
73    pub fn verify(
74        &self,
75        vendor_vk: &VerifyingKey,
76        customer_vk: &VerifyingKey,
77    ) -> crate::Result<()> {
78        let payload_bytes = Self::payload_bytes(&self.payload)?;
79        vendor_vk.verify(
80            &payload_bytes,
81            &Signature::from_bytes(&self.vendor_signature),
82        )?;
83        customer_vk.verify(
84            &payload_bytes,
85            &Signature::from_bytes(&self.customer_signature),
86        )?;
87        Ok(())
88    }
89
90    /// Verifies only the vendor signature.
91    pub fn verify_vendor(&self, vendor_vk: &VerifyingKey) -> crate::Result<()> {
92        let payload_bytes = Self::payload_bytes(&self.payload)?;
93        vendor_vk.verify(
94            &payload_bytes,
95            &Signature::from_bytes(&self.vendor_signature),
96        )
97    }
98
99    pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
100        wire::encode(self)
101    }
102
103    pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
104        wire::decode(bytes)
105    }
106}
107
108/// Vendor-signed bundle awaiting the customer signature.
109pub struct PartialBundle {
110    pub payload: EvidenceBundlePayload,
111    pub vendor_signature: [u8; 64],
112}
113
114impl PartialBundle {
115    /// Adds the customer signature and completes the bundle.
116    pub fn add_customer_signature(
117        self,
118        customer_key: &SigningKey,
119    ) -> crate::Result<EvidenceBundle> {
120        let payload_bytes = EvidenceBundle::payload_bytes(&self.payload)?;
121        let sig = customer_key.sign(&payload_bytes);
122        Ok(EvidenceBundle {
123            payload: self.payload,
124            vendor_signature: self.vendor_signature,
125            customer_signature: sig.to_bytes(),
126        })
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::{crypto::signature::SigningKey, error::Error, message::PROTOCOL_VERSION};
134
135    fn fixture_payload() -> EvidenceBundlePayload {
136        EvidenceBundlePayload {
137            protocol_version: PROTOCOL_VERSION,
138            bundle_id: [0x01; 16],
139            license_request: vec![0x01, 0x02],
140            license_grant: vec![0x03, 0x04],
141            receipt: vec![0x05, 0x06],
142            binding_certificate: vec![0x07, 0x08],
143            terms_hash: [0xab; 32],
144            consent: ConsentRecord {
145                checkboxes_ticked: vec!["terms_of_service".into(), "privacy_policy".into()],
146                consented_at: 1700000000,
147                ip_address: "198.51.100.42".into(),
148            },
149            payment_reference: "ch_test_1234".into(),
150            tsa_token: vec![0xde, 0xad, 0xbe, 0xef],
151            vendor_public_key: [0xee; 32],
152            customer_public_key: [0xcc; 32],
153        }
154    }
155
156    #[test]
157    fn roundtrip_serialization() {
158        let vendor_sk = SigningKey::generate();
159        let customer_sk = SigningKey::generate();
160
161        let partial = EvidenceBundle::sign_vendor(fixture_payload(), &vendor_sk).unwrap();
162        let bundle = partial.add_customer_signature(&customer_sk).unwrap();
163
164        let bytes = bundle.to_bytes().unwrap();
165        let decoded = EvidenceBundle::from_bytes(&bytes).unwrap();
166        assert_eq!(bundle, decoded);
167    }
168
169    #[test]
170    fn verify_signatures_pass() {
171        let vendor_sk = SigningKey::generate();
172        let customer_sk = SigningKey::generate();
173
174        let partial = EvidenceBundle::sign_vendor(fixture_payload(), &vendor_sk).unwrap();
175        let bundle = partial.add_customer_signature(&customer_sk).unwrap();
176
177        bundle
178            .verify(&vendor_sk.verifying_key(), &customer_sk.verifying_key())
179            .unwrap();
180    }
181
182    #[test]
183    fn tampered_payload_fails_verification() {
184        let vendor_sk = SigningKey::generate();
185        let customer_sk = SigningKey::generate();
186
187        let partial = EvidenceBundle::sign_vendor(fixture_payload(), &vendor_sk).unwrap();
188        let mut bundle = partial.add_customer_signature(&customer_sk).unwrap();
189
190        // tamper after signing
191        bundle.payload.payment_reference = "ch_tampered".into();
192
193        let result = bundle.verify(&vendor_sk.verifying_key(), &customer_sk.verifying_key());
194        assert!(result.is_err());
195    }
196
197    #[test]
198    fn wrong_key_fails_vendor_verification() {
199        let vendor_sk = SigningKey::generate();
200        let wrong_sk = SigningKey::generate();
201        let customer_sk = SigningKey::generate();
202
203        let partial = EvidenceBundle::sign_vendor(fixture_payload(), &vendor_sk).unwrap();
204        let bundle = partial.add_customer_signature(&customer_sk).unwrap();
205
206        let result = bundle.verify_vendor(&wrong_sk.verifying_key());
207        assert!(matches!(result, Err(Error::SignatureInvalid)));
208    }
209}