OXYGENHUB
# b8a0399319fc oxygen / crates / oxy-core / src / protocol.rs
22.7 KiB · Rust · last changed Trust policy: policy objects, init/grant/revoke, enforcement on push and fetch, propagation with downgrade protection (spec 0.6) 1 month ago source blame history raw
5b9c6ad4 Andy McCall ↶ 1
//! Transport-agnostic protocol (spec §16): what a client asks of a remote and
2
//! what a server does with a push. `LocalRemote` serves another repository
750251b2 Andy McCall ↶ 3
//! directory in-process, `HttpRemote` speaks to an `oxy-server` over HTTP(S),
4
//! and the server side reuses `Repo::receive_push` and friends unchanged.
5
//!
6
//! Wire messages are deterministic CBOR (`wire` module); over HTTP each is a
7
//! request or response body with content type `application/oxy-cbor`.
5b9c6ad4 Andy McCall ↶ 8
9
use crate::attest::Attest;
750251b2 Andy McCall ↶ 10
use crate::cbor::{self, Value};
5b9c6ad4 Andy McCall ↶ 11
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
12
use crate::identity::Signature;
750251b2 Andy McCall ↶ 13
use crate::objects::{self as o, Change, ObjectError};
83e0677b Andy McCall ↶ 14
use crate::policy::Policy;
5b9c6ad4 Andy McCall ↶ 15
use crate::repo::{Repo, RepoError, Result};
2b5ee371 Andy McCall ↶ 16
use std::path::{Path, PathBuf};
17
use std::sync::Arc;
5b9c6ad4 Andy McCall ↶ 18
750251b2 Andy McCall ↶ 19
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
20
pub const PROTOCOL_VERSION: u64 = 1;
21
5b9c6ad4 Andy McCall ↶ 22
/// An attestation together with the signatures over its ObjectRef.
23
#[derive(Clone, Debug, PartialEq, Eq)]
24
pub struct SignedAttest {
25
    pub attest: Attest,
26
    pub sigs: Vec<Signature>,
27
}
28
83e0677b Andy McCall ↶ 29
/// A policy together with the root signatures over its ObjectRef.
30
#[derive(Clone, Debug, PartialEq, Eq)]
31
pub struct SignedPolicy {
32
    pub policy: Policy,
33
    pub sigs: Vec<Signature>,
34
}
35
36
/// What `refs` returns: every bookmark plus the current policy, if any.
37
#[derive(Clone, Debug, Default)]
38
pub struct RefsResponse {
39
    pub refs: Vec<RemoteRef>,
40
    pub policy: Option<SignedPolicy>,
41
}
42
5b9c6ad4 Andy McCall ↶ 43
/// One reference as a remote reports it.
44
#[derive(Clone, Debug)]
45
pub struct RemoteRef {
46
    pub name: String,
47
    pub state: ObjectRef,
48
    pub heads: Vec<ChangeHash>,
49
    pub attestation: Option<SignedAttest>,
50
}
51
52
/// Everything needed to add a set of changes to a repository.
53
#[derive(Clone, Debug, Default)]
54
pub struct Bundle {
55
    /// In dependency order, each with its individual signatures.
56
    pub changes: Vec<(Change, Vec<Signature>)>,
57
    /// Seals covering the changes.
58
    pub attests: Vec<SignedAttest>,
59
    /// Chunks, blobs, identities, and state objects referenced by the changes.
60
    pub objects: Vec<(ObjectType, Vec<u8>)>,
61
}
62
63
impl Bundle {
64
    pub fn is_empty(&self) -> bool {
65
        self.changes.is_empty() && self.attests.is_empty() && self.objects.is_empty()
66
    }
67
}
68
69
/// A requested bookmark move (spec §10.2, §16.3).
70
#[derive(Clone, Debug)]
71
pub struct RefUpdate {
72
    pub name: String,
73
    pub old_state: ObjectRef,
74
    pub new_state: ObjectRef,
75
    pub force: bool,
76
    pub attestation: Option<SignedAttest>,
77
}
78
79
#[derive(Clone, Debug, PartialEq, Eq)]
80
pub struct RefResult {
81
    pub name: String,
82
    pub ok: bool,
83
    pub reason: String,
84
}
85
86
pub trait Remote {
87
    fn repo_id(&self) -> Result<[u8; 16]>;
83e0677b Andy McCall ↶ 88
    fn refs(&self) -> Result<RefsResponse>;
5b9c6ad4 Andy McCall ↶ 89
    /// Changes in the closure of `want` but not of `have`, in dependency order.
90
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>>;
91
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle>;
83e0677b Andy McCall ↶ 92
    /// Push changes, bookmark updates, and optionally a newer policy. The
93
    /// policy is applied first; a result named `policy` reports it.
94
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>) -> Result<Vec<RefResult>>;
5b9c6ad4 Andy McCall ↶ 95
}
96
750251b2 Andy McCall ↶ 97
// ---------------------------------------------------------------- local
98
5b9c6ad4 Andy McCall ↶ 99
/// Another repository on this machine, served in-process.
100
pub struct LocalRemote {
101
    pub repo: Repo,
102
}
103
104
impl LocalRemote {
105
    pub fn open(path: &Path) -> Result<LocalRemote> {
106
        Ok(LocalRemote { repo: Repo::open(path)? })
107
    }
108
}
109
110
impl Remote for LocalRemote {
111
    fn repo_id(&self) -> Result<[u8; 16]> {
112
        self.repo.id()
113
    }
83e0677b Andy McCall ↶ 114
    fn refs(&self) -> Result<RefsResponse> {
5b9c6ad4 Andy McCall ↶ 115
        self.repo.remote_refs()
116
    }
117
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
118
        self.repo.negotiate(have, want)
119
    }
120
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle> {
121
        self.repo.bundle_for(changes)
122
    }
83e0677b Andy McCall ↶ 123
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>) -> Result<Vec<RefResult>> {
124
        self.repo.receive_push(bundle, updates, policy)
5b9c6ad4 Andy McCall ↶ 125
    }
126
}
127
750251b2 Andy McCall ↶ 128
// ---------------------------------------------------------------- http
129
2b5ee371 Andy McCall ↶ 130
/// How to reach a remote: credentials and trust.
131
#[derive(Clone, Debug, Default)]
132
pub struct RemoteOptions {
133
    /// Bearer token for pushes.
134
    pub token: Option<String>,
135
    /// PEM file with extra root certificates to trust for `https://` remotes
136
    /// (self-hosted servers); the system web roots are always trusted.
137
    pub ca: Option<PathBuf>,
138
}
139
750251b2 Andy McCall ↶ 140
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
141
pub struct HttpRemote {
142
    base: String,
143
    token: Option<String>,
144
    agent: ureq::Agent,
145
}
146
2b5ee371 Andy McCall ↶ 147
/// A TLS client configuration trusting the web roots plus `ca`, if given.
148
fn tls_client_config(ca: Option<&Path>) -> Result<Arc<rustls::ClientConfig>> {
149
    let mut roots = rustls::RootCertStore::empty();
150
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
151
    if let Some(path) = ca {
152
        let pem = std::fs::read(path).map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?;
153
        let certs = rustls_pemfile::certs(&mut pem.as_slice())
154
            .collect::<std::result::Result<Vec<_>, _>>()
155
            .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?;
156
        if certs.is_empty() {
157
            return Err(RepoError::Msg(format!("CA file {} holds no certificates", path.display())));
158
        }
159
        for c in certs {
160
            roots.add(c).map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?;
161
        }
162
    }
163
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
164
        .with_safe_default_protocol_versions()
165
        .map_err(|e| RepoError::Msg(e.to_string()))?
166
        .with_root_certificates(roots)
167
        .with_no_client_auth();
168
    Ok(Arc::new(cfg))
169
}
170
750251b2 Andy McCall ↶ 171
impl HttpRemote {
2b5ee371 Andy McCall ↶ 172
    pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> {
173
        let agent = ureq::AgentBuilder::new().tls_config(tls_client_config(options.ca.as_deref())?).build();
174
        Ok(HttpRemote { base: base.trim_end_matches('/').to_string(), token: options.token.clone(), agent })
750251b2 Andy McCall ↶ 175
    }
176
177
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
178
        let url = format!("{}/oxy/v1/{endpoint}", self.base);
179
        let mut req = if post { self.agent.post(&url) } else { self.agent.get(&url) };
180
        req = req.set("Accept", CONTENT_TYPE);
181
        if let Some(t) = &self.token {
182
            req = req.set("Authorization", &format!("Bearer {t}"));
183
        }
184
        let resp = match body {
185
            Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b),
186
            None => req.call(),
187
        };
188
        match resp {
189
            Ok(r) => {
190
                let mut out = Vec::new();
191
                r.into_reader().read_to_end(&mut out).map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
192
                Ok(out)
193
            }
194
            Err(ureq::Error::Status(code, r)) => {
195
                let text = r.into_string().unwrap_or_default();
196
                Err(RepoError::Msg(format!("{url}: HTTP {code}: {}", text.trim())))
197
            }
198
            Err(e) => Err(RepoError::Msg(format!("{url}: {e}"))),
199
        }
200
    }
201
}
202
203
impl Remote for HttpRemote {
204
    fn repo_id(&self) -> Result<[u8; 16]> {
205
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
206
    }
83e0677b Andy McCall ↶ 207
    fn refs(&self) -> Result<RefsResponse> {
750251b2 Andy McCall ↶ 208
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
209
    }
210
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
211
        wire::decode_hashes(&self.call(true, "negotiate", Some(&wire::encode_negotiate(have, want)))?)
212
    }
213
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle> {
214
        wire::decode_bundle(&self.call(true, "fetch", Some(&wire::encode_hashes(changes)))?)
215
    }
83e0677b Andy McCall ↶ 216
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>) -> Result<Vec<RefResult>> {
217
        wire::decode_push_response(&self.call(true, "push", Some(&wire::encode_push(&bundle, &updates, policy.as_ref())))?)
750251b2 Andy McCall ↶ 218
    }
219
}
220
221
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
2b5ee371 Andy McCall ↶ 222
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
750251b2 Andy McCall ↶ 223
    if url.starts_with("http://") || url.starts_with("https://") {
2b5ee371 Andy McCall ↶ 224
        return Ok(Box::new(HttpRemote::new(url, options)?));
750251b2 Andy McCall ↶ 225
    }
5b9c6ad4 Andy McCall ↶ 226
    let path = url.strip_prefix("file://").unwrap_or(url);
227
    if path.contains("://") {
750251b2 Andy McCall ↶ 228
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
5b9c6ad4 Andy McCall ↶ 229
    }
230
    Ok(Box::new(LocalRemote::open(Path::new(path))?))
231
}
750251b2 Andy McCall ↶ 232
233
// ---------------------------------------------------------------- wire
234
235
/// CBOR encodings of the protocol messages (spec §16.1).
236
pub mod wire {
237
    use super::*;
238
239
    fn schema(msg: &str) -> RepoError {
240
        RepoError::Object(ObjectError::Schema(msg.into()))
241
    }
242
    fn sigs_value(sigs: &[Signature]) -> Value {
243
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
244
    }
245
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
246
        Ok(o::array(v, "signatures")?.iter().map(Signature::from_value).collect::<std::result::Result<Vec<_>, _>>()?)
247
    }
248
    fn hashes_value(hs: &[ObjectRef]) -> Value {
249
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
250
    }
251
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
252
        Ok(o::array(v, "hashes")?.iter().map(|h| o::oref(h, "hash")).collect::<std::result::Result<Vec<_>, _>>()?)
253
    }
254
    fn attest_value(sa: &SignedAttest) -> Value {
255
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
256
    }
257
    fn attest_from(v: &Value) -> Result<SignedAttest> {
258
        let a = o::array(v, "signed attest")?;
259
        if a.len() != 2 {
260
            return Err(schema("signed attest: expected [attest, sigs]"));
261
        }
262
        Ok(SignedAttest { attest: Attest::decode(o::bytes(&a[0], "attest")?)?, sigs: sigs_from(&a[1])? })
263
    }
264
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
265
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
266
    }
267
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
268
        if v.is_null() { Ok(None) } else { attest_from(v).map(Some) }
269
    }
83e0677b Andy McCall ↶ 270
    fn opt_policy_value(sp: Option<&SignedPolicy>) -> Value {
271
        match sp {
272
            Some(sp) => Value::Array(vec![Value::Bytes(sp.policy.encode()), sigs_value(&sp.sigs)]),
273
            None => Value::Null,
274
        }
275
    }
276
    fn opt_policy_from(v: &Value) -> Result<Option<SignedPolicy>> {
277
        if v.is_null() {
278
            return Ok(None);
279
        }
280
        let a = o::array(v, "signed policy")?;
281
        if a.len() != 2 {
282
            return Err(schema("signed policy: expected [policy, sigs]"));
283
        }
284
        Ok(Some(SignedPolicy { policy: Policy::decode(o::bytes(&a[0], "policy")?)?, sigs: sigs_from(&a[1])? }))
285
    }
750251b2 Andy McCall ↶ 286
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
287
        Ok(o::map(v, what)?)
288
    }
289
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
290
        Ok(o::field(m, k, what)?)
291
    }
292
293
    pub fn bundle_value(b: &Bundle) -> Value {
294
        Value::map([
295
            (
296
                0,
297
                Value::Array(b.changes.iter().map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)])).collect()),
298
            ),
299
            (1, Value::Array(b.attests.iter().map(attest_value).collect())),
300
            (
301
                2,
302
                Value::Array(
303
                    b.objects.iter().map(|(t, body)| Value::Array(vec![Value::Text(t.name().into()), Value::Bytes(body.clone())])).collect(),
304
                ),
305
            ),
306
        ])
307
    }
308
309
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
310
        let m = map(v, "bundle")?;
311
        let mut b = Bundle::default();
312
        for c in o::array(field(m, 0, "changes")?, "changes")? {
313
            let pair = o::array(c, "change entry")?;
314
            if pair.len() != 2 {
315
                return Err(schema("change entry: expected [change, sigs]"));
316
            }
317
            b.changes.push((Change::decode(o::bytes(&pair[0], "change")?)?, sigs_from(&pair[1])?));
318
        }
319
        for a in o::array(field(m, 1, "attests")?, "attests")? {
320
            b.attests.push(attest_from(a)?);
321
        }
322
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
323
            let pair = o::array(ob, "object")?;
324
            if pair.len() != 2 {
325
                return Err(schema("object: expected [type, body]"));
326
            }
327
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?).ok_or_else(|| schema("unknown object type"))?;
328
            b.objects.push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
329
        }
330
        Ok(b)
331
    }
332
333
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
334
        cbor::encode(&bundle_value(b))
335
    }
336
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
337
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
338
    }
339
340
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
341
        cbor::encode(&Value::map([
342
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
343
            (1, Value::Bytes(id.to_vec())),
344
            (2, Value::Array(vec![Value::Text("seal".into())])),
345
        ]))
346
    }
347
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
348
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
349
        let m = map(&v, "capabilities")?;
350
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
351
        if !versions.iter().any(|x| x.as_uint() == Some(PROTOCOL_VERSION)) {
352
            return Err(RepoError::Msg("remote does not speak protocol version 1".into()));
353
        }
354
        o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))
355
    }
356
83e0677b Andy McCall ↶ 357
    pub fn encode_refs(id: [u8; 16], resp: &RefsResponse) -> Vec<u8> {
750251b2 Andy McCall ↶ 358
        cbor::encode(&Value::map([
359
            (
360
                0,
361
                Value::Array(
83e0677b Andy McCall ↶ 362
                    resp.refs
363
                        .iter()
750251b2 Andy McCall ↶ 364
                        .map(|r| {
365
                            Value::map([
366
                                (0, Value::Text(r.name.clone())),
367
                                (1, Value::Bytes(r.state.0.to_vec())),
368
                                (2, hashes_value(&r.heads)),
369
                                (3, opt_attest_value(&r.attestation)),
370
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
371
                            ])
372
                        })
373
                        .collect(),
374
                ),
375
            ),
376
            (1, Value::Bytes(id.to_vec())),
83e0677b Andy McCall ↶ 377
            (2, opt_policy_value(resp.policy.as_ref())),
750251b2 Andy McCall ↶ 378
        ]))
379
    }
83e0677b Andy McCall ↶ 380
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], RefsResponse)> {
750251b2 Andy McCall ↶ 381
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
382
        let m = map(&v, "refs")?;
383
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))?;
384
        let mut refs = Vec::new();
385
        for r in o::array(field(m, 0, "refs")?, "refs")? {
386
            let rm = map(r, "ref")?;
387
            let ls = field(rm, 4, "last serial")?;
388
            refs.push(RemoteRef {
389
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
390
                state: o::oref(field(rm, 1, "state")?, "state")?,
391
                heads: hashes_from(field(rm, 2, "heads")?)?,
392
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
393
                last_serial: if ls.is_null() { None } else { Some(o::uint(ls, "last serial")?) },
394
            });
395
        }
83e0677b Andy McCall ↶ 396
        let policy = match m.get(&2) {
397
            Some(p) => opt_policy_from(p)?,
398
            None => None,
399
        };
400
        Ok((id, RefsResponse { refs, policy }))
750251b2 Andy McCall ↶ 401
    }
402
403
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
404
        cbor::encode(&Value::map([(0, hashes_value(have)), (1, hashes_value(want))]))
405
    }
406
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
407
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
408
        let m = map(&v, "negotiate")?;
409
        Ok((hashes_from(field(m, 0, "have")?)?, hashes_from(field(m, 1, "want")?)?))
410
    }
411
412
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
413
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
414
    }
415
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
416
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
417
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
418
    }
419
83e0677b Andy McCall ↶ 420
    pub fn encode_push(bundle: &Bundle, updates: &[RefUpdate], policy: Option<&SignedPolicy>) -> Vec<u8> {
750251b2 Andy McCall ↶ 421
        cbor::encode(&Value::map([
422
            (0, bundle_value(bundle)),
83e0677b Andy McCall ↶ 423
            (2, opt_policy_value(policy)),
750251b2 Andy McCall ↶ 424
            (
425
                1,
426
                Value::Array(
427
                    updates
428
                        .iter()
429
                        .map(|u| {
430
                            Value::map([
431
                                (0, Value::Text(u.name.clone())),
432
                                (1, Value::Bytes(u.old_state.0.to_vec())),
433
                                (2, Value::Bytes(u.new_state.0.to_vec())),
434
                                (3, Value::Bool(u.force)),
435
                                (4, opt_attest_value(&u.attestation)),
436
                            ])
437
                        })
438
                        .collect(),
439
                ),
440
            ),
441
        ]))
442
    }
83e0677b Andy McCall ↶ 443
    pub fn decode_push(bytes: &[u8]) -> Result<(Bundle, Vec<RefUpdate>, Option<SignedPolicy>)> {
750251b2 Andy McCall ↶ 444
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
445
        let m = map(&v, "push")?;
446
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
447
        let mut updates = Vec::new();
448
        for u in o::array(field(m, 1, "updates")?, "updates")? {
449
            let um = map(u, "update")?;
450
            updates.push(RefUpdate {
451
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
452
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
453
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
454
                force: field(um, 3, "force")?.as_bool().ok_or_else(|| schema("force: expected bool"))?,
455
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
456
            });
457
        }
83e0677b Andy McCall ↶ 458
        let policy = match m.get(&2) {
459
            Some(p) => opt_policy_from(p)?,
460
            None => None,
461
        };
462
        Ok((bundle, updates, policy))
750251b2 Andy McCall ↶ 463
    }
464
465
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
466
        cbor::encode(&Value::map([(
467
            0,
468
            Value::Array(
469
                results
470
                    .iter()
471
                    .map(|r| Value::map([(0, Value::Text(r.name.clone())), (1, Value::Bool(r.ok)), (2, Value::Text(r.reason.clone()))]))
472
                    .collect(),
473
            ),
474
        )]))
475
    }
476
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
477
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
478
        let m = map(&v, "push response")?;
479
        let mut out = Vec::new();
480
        for r in o::array(field(m, 0, "results")?, "results")? {
481
            let rm = map(r, "result")?;
482
            out.push(RefResult {
483
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
484
                ok: field(rm, 1, "ok")?.as_bool().ok_or_else(|| schema("ok: expected bool"))?,
485
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
486
            });
487
        }
488
        Ok(out)
489
    }
490
}
491
492
#[cfg(test)]
493
mod tests {
494
    use super::*;
495
    use crate::attest::AttestBody;
496
    use crate::objects::Hlc;
497
498
    #[test]
499
    fn wire_round_trips() {
500
        let c = crate::objects::tests::sample_change();
501
        let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
502
        let id = sk.identity("a", "b", 0);
503
        let key = id.hash();
504
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
505
        let seal = Attest::seal([3; 16], vec![c.hash()]);
506
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
507
        let bundle = Bundle {
508
            changes: vec![(c.clone(), vec![sig.clone()])],
509
            attests: vec![SignedAttest { attest: seal.clone(), sigs: vec![seal_sig] }],
510
            objects: vec![(ObjectType::Identity, id.encode()), (ObjectType::Chunk, b"xyz".to_vec())],
511
        };
512
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
513
        assert_eq!(d.changes, bundle.changes);
514
        assert_eq!(d.attests, bundle.attests);
515
        assert_eq!(d.objects, bundle.objects);
516
517
        let att = Attest { repo: [3; 16], hlc: Hlc::default(), body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 4 } };
518
        let refs = vec![
519
            RemoteRef { name: "main".into(), state: ObjectRef([2; 32]), heads: vec![c.hash()], attestation: Some(SignedAttest { attest: att.clone(), sigs: vec![sig.clone()] }), last_serial: Some(4) },
520
            RemoteRef { name: "dev".into(), state: ObjectRef([5; 32]), heads: vec![], attestation: None, last_serial: None },
521
        ];
83e0677b Andy McCall ↶ 522
        let policy = Policy::single_root([3; 16], id.clone(), 99);
523
        let psig = sk.sign(crate::identity::Domain::Policy, &key, &policy.hash());
524
        let resp = RefsResponse { refs, policy: Some(SignedPolicy { policy: policy.clone(), sigs: vec![psig.clone()] }) };
525
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &resp)).unwrap();
750251b2 Andy McCall ↶ 526
        assert_eq!(id_back, [3; 16]);
83e0677b Andy McCall ↶ 527
        assert_eq!(back.refs.len(), 2);
528
        assert_eq!(back.refs[0].attestation.as_ref().unwrap().attest, att);
529
        assert_eq!(back.refs[1].last_serial, None);
530
        assert_eq!(back.policy.as_ref().unwrap().policy, policy);
750251b2 Andy McCall ↶ 531
532
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(&[ObjectRef([1; 32])], &[ObjectRef([2; 32])])).unwrap();
533
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
534
        assert_eq!(wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(), vec![c.hash()]);
535
536
        let up = RefUpdate { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), force: true, attestation: Some(SignedAttest { attest: att, sigs: vec![sig] }) };
83e0677b Andy McCall ↶ 537
        let sp = SignedPolicy { policy, sigs: vec![psig] };
538
        let (b2, ups, pol) = wire::decode_push(&wire::encode_push(&bundle, std::slice::from_ref(&up), Some(&sp))).unwrap();
539
        assert_eq!(pol, Some(sp));
750251b2 Andy McCall ↶ 540
        assert_eq!(b2.changes.len(), 1);
541
        assert_eq!(ups[0].name, "main");
542
        assert!(ups[0].force);
543
        assert_eq!(ups[0].attestation, up.attestation);
544
        let res = vec![RefResult { name: "main".into(), ok: false, reason: "stale".into() }];
545
        assert_eq!(wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(), res);
546
        assert_eq!(wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(), [7; 16]);
547
    }
548
}