OXYGENHUB
# eae595568832 oxygen / crates / oxy-core / src / protocol.rs
34.5 KiB · Rust · last changed Delegate rule on fetch and tags too 1 month ago source blame history raw
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
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`.
8
9
use crate::attest::Attest;
10
use crate::cbor::{self, Value};
11
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
12
use crate::identity::Signature;
13
use crate::objects::{self as o, Change, ObjectError};
14
use crate::policy::Policy;
15
use crate::repo::{Repo, RepoError, Result};
16
use std::path::{Path, PathBuf};
17
use std::sync::Arc;
18
19
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
20
pub const PROTOCOL_VERSION: u64 = 1;
21
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
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
impl SignedPolicy {
37
    /// The proposal-file encoding (`wire::policy_value`).
38
    pub fn encode(&self) -> Vec<u8> {
39
        crate::cbor::encode(&wire::policy_value(self))
40
    }
41
    pub fn decode(b: &[u8]) -> Result<SignedPolicy> {
42
        wire::policy_from(
43
            &crate::cbor::decode(b).map_err(|e| RepoError::Object(ObjectError::from(e)))?,
44
        )
45
    }
46
}
47
48
/// What `refs` returns: every bookmark, every tag, and the current policy.
49
#[derive(Clone, Debug, Default)]
50
pub struct RefsResponse {
51
    pub refs: Vec<RemoteRef>,
52
    pub policy: Option<SignedPolicy>,
53
    pub tags: Vec<TagRef>,
54
    /// Earlier policies, ascending by serial, so a client behind by more
55
    /// than one update can verify each step (spec §14.5).
56
    pub history: Vec<SignedPolicy>,
57
}
58
59
/// A tag as a remote reports it (spec §10.1).
60
#[derive(Clone, Debug, PartialEq, Eq)]
61
pub struct TagRef {
62
    pub name: String,
63
    pub state: ObjectRef,
64
    pub heads: Vec<ChangeHash>,
65
    pub attestation: Option<SignedAttest>,
66
}
67
68
/// One reference as a remote reports it.
69
#[derive(Clone, Debug)]
70
pub struct RemoteRef {
71
    pub name: String,
72
    pub state: ObjectRef,
73
    pub heads: Vec<ChangeHash>,
74
    pub attestation: Option<SignedAttest>,
75
}
76
77
/// Everything needed to add a set of changes to a repository.
78
#[derive(Clone, Debug, Default)]
79
pub struct Bundle {
80
    /// In dependency order, each with its individual signatures.
81
    pub changes: Vec<(Change, Vec<Signature>)>,
82
    /// Seals covering the changes.
83
    pub attests: Vec<SignedAttest>,
84
    /// Chunks, blobs, identities, and state objects referenced by the changes.
85
    pub objects: Vec<(ObjectType, Vec<u8>)>,
86
    /// Policies between the receiver's and the one sent with the push,
87
    /// ascending by serial (spec §14.5).
88
    pub policies: Vec<SignedPolicy>,
89
}
90
91
impl Bundle {
92
    pub fn is_empty(&self) -> bool {
93
        self.changes.is_empty() && self.attests.is_empty() && self.objects.is_empty()
94
    }
95
}
96
97
/// A requested bookmark move (spec §10.2, §16.3).
98
#[derive(Clone, Debug)]
99
pub struct RefUpdate {
100
    pub name: String,
101
    pub old_state: ObjectRef,
102
    pub new_state: ObjectRef,
103
    pub force: bool,
104
    pub attestation: Option<SignedAttest>,
105
}
106
107
#[derive(Clone, Debug, PartialEq, Eq)]
108
pub struct RefResult {
109
    pub name: String,
110
    pub ok: bool,
111
    pub reason: String,
112
}
113
114
pub trait Remote {
115
    fn repo_id(&self) -> Result<[u8; 16]>;
116
    fn refs(&self) -> Result<RefsResponse>;
117
    /// Changes in the closure of `want` but not of `have`, in dependency order.
118
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>>;
119
    /// The changes with their signatures, seals, identities, and blobs; blobs
120
    /// larger than `max_blob` bytes are left out for lazy fetching (spec §13.5).
121
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle>;
122
    /// Individual objects by reference (spec §16.1 `objects`); a blob comes
123
    /// with its chunks.
124
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>>;
125
    /// A checkpoint of exactly `heads` (spec §16.1 `checkpoint`): the
126
    /// checkpoint object, its payload blob, and the blob's chunks.
127
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle>;
128
    /// The changes within `depth` dependency steps of `heads` (spec §17).
129
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>>;
130
    /// Push changes, bookmark updates, new tags, and optionally a newer
131
    /// policy. The policy is applied first; a result named `policy` reports
132
    /// it, and each tag reports as `tags/<name>`.
133
    fn push(
134
        &self,
135
        bundle: Bundle,
136
        updates: Vec<RefUpdate>,
137
        policy: Option<SignedPolicy>,
138
        tags: Vec<TagRef>,
139
    ) -> Result<Vec<RefResult>>;
140
}
141
142
// ---------------------------------------------------------------- local
143
144
/// Another repository on this machine, served in-process.
145
pub struct LocalRemote {
146
    pub repo: Repo,
147
    /// The pushing repository's id, which binds an unbound target on the
148
    /// first push, as `Oxy-Repo-Id` does over HTTP.
149
    pub claimed_id: Option<[u8; 16]>,
150
}
151
152
impl LocalRemote {
153
    pub fn open(path: &Path) -> Result<LocalRemote> {
154
        Ok(LocalRemote {
155
            repo: Repo::open(path)?,
156
            claimed_id: None,
157
        })
158
    }
159
}
160
161
impl Remote for LocalRemote {
162
    fn repo_id(&self) -> Result<[u8; 16]> {
163
        self.repo.id()
164
    }
165
    fn refs(&self) -> Result<RefsResponse> {
166
        self.repo.remote_refs()
167
    }
168
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
169
        self.repo.negotiate(have, want)
170
    }
171
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
172
        self.repo.bundle_for_filtered(changes, max_blob)
173
    }
174
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
175
        self.repo.objects_for(refs)
176
    }
177
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
178
        self.repo.checkpoint_bundle_for(heads)
179
    }
180
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
181
        self.repo.shallow_changes(heads, depth)
182
    }
183
    fn push(
184
        &self,
185
        bundle: Bundle,
186
        updates: Vec<RefUpdate>,
187
        policy: Option<SignedPolicy>,
188
        tags: Vec<TagRef>,
189
    ) -> Result<Vec<RefResult>> {
190
        if self.repo.is_unbound() {
191
            match self.claimed_id {
192
                Some(id) => self.repo.bind_id(id)?,
193
                None => {
194
                    return Err(RepoError::Msg(
195
                        "the remote is unbound and this client did not say which repository it is"
196
                            .into(),
197
                    ))
198
                }
199
            }
200
        }
201
        self.repo.receive_push(bundle, updates, policy, tags)
202
    }
203
}
204
205
// ---------------------------------------------------------------- http
206
207
/// How to reach a remote: credentials and trust.
208
#[derive(Clone, Debug, Default)]
209
pub struct RemoteOptions {
210
    /// Bearer token for pushes.
211
    pub token: Option<String>,
212
    /// PEM file with extra root certificates to trust for `https://` remotes
213
    /// (self-hosted servers); the system web roots are always trusted.
214
    pub ca: Option<PathBuf>,
215
    /// The local repository's id, sent as `Oxy-Repo-Id` so an unbound
216
    /// hosted repository can adopt it on the first push (spec §16.1).
217
    pub repo_id: Option<[u8; 16]>,
218
}
219
220
/// The request header carrying the client's repository id.
221
pub const REPO_ID_HEADER: &str = "Oxy-Repo-Id";
222
223
/// The id of a hosted repository that has not adopted one yet.
224
pub const UNBOUND_ID: [u8; 16] = [0; 16];
225
226
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
227
pub struct HttpRemote {
228
    base: String,
229
    token: Option<String>,
230
    repo_id: Option<[u8; 16]>,
231
    agent: ureq::Agent,
232
}
233
234
/// A TLS client configuration trusting the web roots plus `ca`, if given.
235
fn tls_client_config(ca: Option<&Path>) -> Result<Arc<rustls::ClientConfig>> {
236
    let mut roots = rustls::RootCertStore::empty();
237
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
238
    if let Some(path) = ca {
239
        let pem = std::fs::read(path)
240
            .map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?;
241
        let certs = rustls_pemfile::certs(&mut pem.as_slice())
242
            .collect::<std::result::Result<Vec<_>, _>>()
243
            .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?;
244
        if certs.is_empty() {
245
            return Err(RepoError::Msg(format!(
246
                "CA file {} holds no certificates",
247
                path.display()
248
            )));
249
        }
250
        for c in certs {
251
            roots
252
                .add(c)
253
                .map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?;
254
        }
255
    }
256
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(
257
        rustls::crypto::ring::default_provider(),
258
    ))
259
    .with_safe_default_protocol_versions()
260
    .map_err(|e| RepoError::Msg(e.to_string()))?
261
    .with_root_certificates(roots)
262
    .with_no_client_auth();
263
    Ok(Arc::new(cfg))
264
}
265
266
impl HttpRemote {
267
    pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> {
268
        let agent = ureq::AgentBuilder::new()
269
            .tls_config(tls_client_config(options.ca.as_deref())?)
270
            .build();
271
        Ok(HttpRemote {
272
            base: base.trim_end_matches('/').to_string(),
273
            token: options.token.clone(),
274
            repo_id: options.repo_id,
275
            agent,
276
        })
277
    }
278
279
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
280
        let url = format!("{}/oxy/v1/{endpoint}", self.base);
281
        let mut req = if post {
282
            self.agent.post(&url)
283
        } else {
284
            self.agent.get(&url)
285
        };
286
        req = req.set("Accept", CONTENT_TYPE);
287
        if let Some(t) = &self.token {
288
            req = req.set("Authorization", &format!("Bearer {t}"));
289
        }
290
        if let Some(id) = &self.repo_id {
291
            req = req.set(REPO_ID_HEADER, &hex::encode(id));
292
        }
293
        let resp = match body {
294
            Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b),
295
            None => req.call(),
296
        };
297
        match resp {
298
            Ok(r) => {
299
                let mut out = Vec::new();
300
                r.into_reader()
301
                    .read_to_end(&mut out)
302
                    .map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
303
                Ok(out)
304
            }
305
            Err(ureq::Error::Status(code, r)) => {
306
                let text = r.into_string().unwrap_or_default();
307
                Err(RepoError::Msg(format!(
308
                    "{url}: HTTP {code}: {}",
309
                    text.trim()
310
                )))
311
            }
312
            Err(e) => Err(RepoError::Msg(format!("{url}: {e}"))),
313
        }
314
    }
315
}
316
317
impl Remote for HttpRemote {
318
    fn repo_id(&self) -> Result<[u8; 16]> {
319
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
320
    }
321
    fn refs(&self) -> Result<RefsResponse> {
322
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
323
    }
324
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
325
        wire::decode_hashes(&self.call(
326
            true,
327
            "negotiate",
328
            Some(&wire::encode_negotiate(have, want)),
329
        )?)
330
    }
331
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
332
        wire::decode_bundle(&self.call(
333
            true,
334
            "fetch",
335
            Some(&wire::encode_fetch(changes, max_blob)),
336
        )?)
337
    }
338
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
339
        Ok(
340
            wire::decode_bundle(&self.call(true, "objects", Some(&wire::encode_hashes(refs)))?)?
341
                .objects,
342
        )
343
    }
344
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
345
        wire::decode_bundle(&self.call(true, "checkpoint", Some(&wire::encode_hashes(heads)))?)
346
    }
347
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
348
        wire::decode_hashes(&self.call(
349
            true,
350
            "shallow",
351
            Some(&wire::encode_shallow(heads, depth)),
352
        )?)
353
    }
354
    fn push(
355
        &self,
356
        bundle: Bundle,
357
        updates: Vec<RefUpdate>,
358
        policy: Option<SignedPolicy>,
359
        tags: Vec<TagRef>,
360
    ) -> Result<Vec<RefResult>> {
361
        wire::decode_push_response(&self.call(
362
            true,
363
            "push",
364
            Some(&wire::encode_push(
365
                &bundle,
366
                &updates,
367
                policy.as_ref(),
368
                &tags,
369
            )),
370
        )?)
371
    }
372
}
373
374
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
375
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
376
    if url.starts_with("http://") || url.starts_with("https://") {
377
        return Ok(Box::new(HttpRemote::new(url, options)?));
378
    }
379
    let path = url.strip_prefix("file://").unwrap_or(url);
380
    if path.contains("://") {
381
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
382
    }
383
    let mut local = LocalRemote::open(Path::new(path))?;
384
    local.claimed_id = options.repo_id;
385
    Ok(Box::new(local))
386
}
387
388
// ---------------------------------------------------------------- wire
389
390
/// CBOR encodings of the protocol messages (spec §16.1).
391
pub mod wire {
392
    use super::*;
393
394
    fn schema(msg: &str) -> RepoError {
395
        RepoError::Object(ObjectError::Schema(msg.into()))
396
    }
397
    fn sigs_value(sigs: &[Signature]) -> Value {
398
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
399
    }
400
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
401
        Ok(o::array(v, "signatures")?
402
            .iter()
403
            .map(Signature::from_value)
404
            .collect::<std::result::Result<Vec<_>, _>>()?)
405
    }
406
    fn hashes_value(hs: &[ObjectRef]) -> Value {
407
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
408
    }
409
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
410
        Ok(o::array(v, "hashes")?
411
            .iter()
412
            .map(|h| o::oref(h, "hash"))
413
            .collect::<std::result::Result<Vec<_>, _>>()?)
414
    }
415
    fn attest_value(sa: &SignedAttest) -> Value {
416
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
417
    }
418
    fn attest_from(v: &Value) -> Result<SignedAttest> {
419
        let a = o::array(v, "signed attest")?;
420
        if a.len() != 2 {
421
            return Err(schema("signed attest: expected [attest, sigs]"));
422
        }
423
        Ok(SignedAttest {
424
            attest: Attest::decode(o::bytes(&a[0], "attest")?)?,
425
            sigs: sigs_from(&a[1])?,
426
        })
427
    }
428
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
429
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
430
    }
431
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
432
        if v.is_null() {
433
            Ok(None)
434
        } else {
435
            attest_from(v).map(Some)
436
        }
437
    }
438
    /// `[policy bytes, [signature]]`: also the format of a policy proposal
439
    /// file exchanged between root holders (spec §14.5).
440
    pub fn policy_value(sp: &SignedPolicy) -> Value {
441
        Value::Array(vec![Value::Bytes(sp.policy.encode()), sigs_value(&sp.sigs)])
442
    }
443
    pub fn policy_from(v: &Value) -> Result<SignedPolicy> {
444
        let a = o::array(v, "signed policy")?;
445
        if a.len() != 2 {
446
            return Err(schema("signed policy: expected [policy, sigs]"));
447
        }
448
        Ok(SignedPolicy {
449
            policy: Policy::decode(o::bytes(&a[0], "policy")?)?,
450
            sigs: sigs_from(&a[1])?,
451
        })
452
    }
453
    fn opt_policy_value(sp: Option<&SignedPolicy>) -> Value {
454
        sp.map(policy_value).unwrap_or(Value::Null)
455
    }
456
    fn opt_policy_from(v: &Value) -> Result<Option<SignedPolicy>> {
457
        if v.is_null() {
458
            Ok(None)
459
        } else {
460
            policy_from(v).map(Some)
461
        }
462
    }
463
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
464
        Ok(o::map(v, what)?)
465
    }
466
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
467
        Ok(o::field(m, k, what)?)
468
    }
469
470
    pub fn bundle_value(b: &Bundle) -> Value {
471
        Value::map([
472
            (
473
                0,
474
                Value::Array(
475
                    b.changes
476
                        .iter()
477
                        .map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)]))
478
                        .collect(),
479
                ),
480
            ),
481
            (
482
                1,
483
                Value::Array(b.attests.iter().map(attest_value).collect()),
484
            ),
485
            (
486
                2,
487
                Value::Array(
488
                    b.objects
489
                        .iter()
490
                        .map(|(t, body)| {
491
                            Value::Array(vec![
492
                                Value::Text(t.name().into()),
493
                                Value::Bytes(body.clone()),
494
                            ])
495
                        })
496
                        .collect(),
497
                ),
498
            ),
499
            (
500
                3,
501
                Value::Array(b.policies.iter().map(policy_value).collect()),
502
            ),
503
        ])
504
    }
505
506
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
507
        let m = map(v, "bundle")?;
508
        let mut b = Bundle::default();
509
        for c in o::array(field(m, 0, "changes")?, "changes")? {
510
            let pair = o::array(c, "change entry")?;
511
            if pair.len() != 2 {
512
                return Err(schema("change entry: expected [change, sigs]"));
513
            }
514
            b.changes.push((
515
                Change::decode(o::bytes(&pair[0], "change")?)?,
516
                sigs_from(&pair[1])?,
517
            ));
518
        }
519
        for a in o::array(field(m, 1, "attests")?, "attests")? {
520
            b.attests.push(attest_from(a)?);
521
        }
522
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
523
            let pair = o::array(ob, "object")?;
524
            if pair.len() != 2 {
525
                return Err(schema("object: expected [type, body]"));
526
            }
527
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?)
528
                .ok_or_else(|| schema("unknown object type"))?;
529
            b.objects
530
                .push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
531
        }
532
        if let Some(ps) = m.get(&3) {
533
            for p in o::array(ps, "policies")? {
534
                b.policies.push(policy_from(p)?);
535
            }
536
        }
537
        Ok(b)
538
    }
539
540
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
541
        cbor::encode(&bundle_value(b))
542
    }
543
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
544
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
545
    }
546
547
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
548
        cbor::encode(&Value::map([
549
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
550
            (1, Value::Bytes(id.to_vec())),
551
            (2, Value::Array(vec![Value::Text("seal".into())])),
552
        ]))
553
    }
554
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
555
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
556
        let m = map(&v, "capabilities")?;
557
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
558
        if !versions
559
            .iter()
560
            .any(|x| x.as_uint() == Some(PROTOCOL_VERSION))
561
        {
562
            return Err(RepoError::Msg(
563
                "remote does not speak protocol version 1".into(),
564
            ));
565
        }
566
        o::bytes(field(m, 1, "repo id")?, "repo id")?
567
            .try_into()
568
            .map_err(|_| schema("repo id: expected 16 bytes"))
569
    }
570
571
    fn tags_value(tags: &[TagRef]) -> Value {
572
        Value::Array(
573
            tags.iter()
574
                .map(|t| {
575
                    Value::map([
576
                        (0, Value::Text(t.name.clone())),
577
                        (1, Value::Bytes(t.state.0.to_vec())),
578
                        (2, hashes_value(&t.heads)),
579
                        (3, opt_attest_value(&t.attestation)),
580
                    ])
581
                })
582
                .collect(),
583
        )
584
    }
585
    fn tags_from(v: &Value) -> Result<Vec<TagRef>> {
586
        let mut out = Vec::new();
587
        for t in o::array(v, "tags")? {
588
            let tm = map(t, "tag")?;
589
            out.push(TagRef {
590
                name: o::text(field(tm, 0, "name")?, "name")?.to_owned(),
591
                state: o::oref(field(tm, 1, "state")?, "state")?,
592
                heads: hashes_from(field(tm, 2, "heads")?)?,
593
                attestation: opt_attest_from(field(tm, 3, "attestation")?)?,
594
            });
595
        }
596
        Ok(out)
597
    }
598
599
    pub fn encode_refs(id: [u8; 16], resp: &RefsResponse) -> Vec<u8> {
600
        cbor::encode(&Value::map([
601
            (
602
                0,
603
                Value::Array(
604
                    resp.refs
605
                        .iter()
606
                        .map(|r| {
607
                            Value::map([
608
                                (0, Value::Text(r.name.clone())),
609
                                (1, Value::Bytes(r.state.0.to_vec())),
610
                                (2, hashes_value(&r.heads)),
611
                                (3, opt_attest_value(&r.attestation)),
612
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
613
                            ])
614
                        })
615
                        .collect(),
616
                ),
617
            ),
618
            (1, Value::Bytes(id.to_vec())),
619
            (2, opt_policy_value(resp.policy.as_ref())),
620
            (3, tags_value(&resp.tags)),
621
            (
622
                4,
623
                Value::Array(resp.history.iter().map(policy_value).collect()),
624
            ),
625
        ]))
626
    }
627
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], RefsResponse)> {
628
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
629
        let m = map(&v, "refs")?;
630
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?
631
            .try_into()
632
            .map_err(|_| schema("repo id: expected 16 bytes"))?;
633
        let mut refs = Vec::new();
634
        for r in o::array(field(m, 0, "refs")?, "refs")? {
635
            let rm = map(r, "ref")?;
636
            let ls = field(rm, 4, "last serial")?;
637
            refs.push(RemoteRef {
638
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
639
                state: o::oref(field(rm, 1, "state")?, "state")?,
640
                heads: hashes_from(field(rm, 2, "heads")?)?,
641
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
642
                last_serial: if ls.is_null() {
643
                    None
644
                } else {
645
                    Some(o::uint(ls, "last serial")?)
646
                },
647
            });
648
        }
649
        let policy = match m.get(&2) {
650
            Some(p) => opt_policy_from(p)?,
651
            None => None,
652
        };
653
        let tags = match m.get(&3) {
654
            Some(t) => tags_from(t)?,
655
            None => Vec::new(),
656
        };
657
        let mut history = Vec::new();
658
        if let Some(h) = m.get(&4) {
659
            for p in o::array(h, "history")? {
660
                history.push(policy_from(p)?);
661
            }
662
        }
663
        Ok((
664
            id,
665
            RefsResponse {
666
                refs,
667
                policy,
668
                tags,
669
                history,
670
            },
671
        ))
672
    }
673
674
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
675
        cbor::encode(&Value::map([
676
            (0, hashes_value(have)),
677
            (1, hashes_value(want)),
678
        ]))
679
    }
680
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
681
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
682
        let m = map(&v, "negotiate")?;
683
        Ok((
684
            hashes_from(field(m, 0, "have")?)?,
685
            hashes_from(field(m, 1, "want")?)?,
686
        ))
687
    }
688
689
    /// `fetch` request: `{0: [changes], 1: max blob size or null}`.
690
    pub fn encode_fetch(hs: &[ChangeHash], max_blob: Option<u64>) -> Vec<u8> {
691
        cbor::encode(&Value::map([
692
            (0, hashes_value(hs)),
693
            (1, max_blob.map(Value::Uint).unwrap_or(Value::Null)),
694
        ]))
695
    }
696
    pub fn decode_fetch(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Option<u64>)> {
697
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
698
        let m = map(&v, "fetch")?;
699
        let max = match m.get(&1) {
700
            Some(x) if !x.is_null() => Some(o::uint(x, "max blob")?),
701
            _ => None,
702
        };
703
        Ok((hashes_from(field(m, 0, "changes")?)?, max))
704
    }
705
706
    /// `shallow` request: `{0: [heads], 1: depth}`.
707
    pub fn encode_shallow(hs: &[ChangeHash], depth: u32) -> Vec<u8> {
708
        cbor::encode(&Value::map([
709
            (0, hashes_value(hs)),
710
            (1, Value::Uint(depth as u64)),
711
        ]))
712
    }
713
    pub fn decode_shallow(bytes: &[u8]) -> Result<(Vec<ChangeHash>, u32)> {
714
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
715
        let m = map(&v, "shallow")?;
716
        Ok((
717
            hashes_from(field(m, 0, "heads")?)?,
718
            o::u32_(field(m, 1, "depth")?, "depth")?,
719
        ))
720
    }
721
722
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
723
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
724
    }
725
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
726
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
727
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
728
    }
729
730
    pub fn encode_push(
731
        bundle: &Bundle,
732
        updates: &[RefUpdate],
733
        policy: Option<&SignedPolicy>,
734
        tags: &[TagRef],
735
    ) -> Vec<u8> {
736
        cbor::encode(&Value::map([
737
            (0, bundle_value(bundle)),
738
            (2, opt_policy_value(policy)),
739
            (3, tags_value(tags)),
740
            (
741
                1,
742
                Value::Array(
743
                    updates
744
                        .iter()
745
                        .map(|u| {
746
                            Value::map([
747
                                (0, Value::Text(u.name.clone())),
748
                                (1, Value::Bytes(u.old_state.0.to_vec())),
749
                                (2, Value::Bytes(u.new_state.0.to_vec())),
750
                                (3, Value::Bool(u.force)),
751
                                (4, opt_attest_value(&u.attestation)),
752
                            ])
753
                        })
754
                        .collect(),
755
                ),
756
            ),
757
        ]))
758
    }
759
    /// A decoded push: bundle, bookmark updates, policy, tags.
760
    pub type PushParts = (Bundle, Vec<RefUpdate>, Option<SignedPolicy>, Vec<TagRef>);
761
762
    pub fn decode_push(bytes: &[u8]) -> Result<PushParts> {
763
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
764
        let m = map(&v, "push")?;
765
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
766
        let mut updates = Vec::new();
767
        for u in o::array(field(m, 1, "updates")?, "updates")? {
768
            let um = map(u, "update")?;
769
            updates.push(RefUpdate {
770
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
771
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
772
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
773
                force: field(um, 3, "force")?
774
                    .as_bool()
775
                    .ok_or_else(|| schema("force: expected bool"))?,
776
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
777
            });
778
        }
779
        let policy = match m.get(&2) {
780
            Some(p) => opt_policy_from(p)?,
781
            None => None,
782
        };
783
        let tags = match m.get(&3) {
784
            Some(t) => tags_from(t)?,
785
            None => Vec::new(),
786
        };
787
        Ok((bundle, updates, policy, tags))
788
    }
789
790
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
791
        cbor::encode(&Value::map([(
792
            0,
793
            Value::Array(
794
                results
795
                    .iter()
796
                    .map(|r| {
797
                        Value::map([
798
                            (0, Value::Text(r.name.clone())),
799
                            (1, Value::Bool(r.ok)),
800
                            (2, Value::Text(r.reason.clone())),
801
                        ])
802
                    })
803
                    .collect(),
804
            ),
805
        )]))
806
    }
807
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
808
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
809
        let m = map(&v, "push response")?;
810
        let mut out = Vec::new();
811
        for r in o::array(field(m, 0, "results")?, "results")? {
812
            let rm = map(r, "result")?;
813
            out.push(RefResult {
814
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
815
                ok: field(rm, 1, "ok")?
816
                    .as_bool()
817
                    .ok_or_else(|| schema("ok: expected bool"))?,
818
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
819
            });
820
        }
821
        Ok(out)
822
    }
823
}
824
825
#[cfg(test)]
826
mod tests {
827
    use super::*;
828
    use crate::attest::AttestBody;
829
    use crate::objects::Hlc;
830
831
    #[test]
832
    fn wire_round_trips() {
833
        let c = crate::objects::tests::sample_change();
834
        let sk = crate::identity::SecretKey {
835
            ed_seed: [1; 32],
836
            ml_seed: [2; 32],
837
        };
838
        let id = sk.identity("a", "b", 0);
839
        let key = id.hash();
840
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
841
        let seal = Attest::seal([3; 16], vec![c.hash()]);
842
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
843
        let policy = Policy::single_root([3; 16], id.clone(), 99);
844
        let psig = sk.sign(crate::identity::Domain::Policy, &key, &policy.hash());
845
        let bundle = Bundle {
846
            changes: vec![(c.clone(), vec![sig.clone()])],
847
            attests: vec![SignedAttest {
848
                attest: seal.clone(),
849
                sigs: vec![seal_sig],
850
            }],
851
            objects: vec![
852
                (ObjectType::Identity, id.encode()),
853
                (ObjectType::Chunk, b"xyz".to_vec()),
854
            ],
855
            policies: vec![SignedPolicy {
856
                policy: policy.clone(),
857
                sigs: vec![psig.clone()],
858
            }],
859
        };
860
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
861
        assert_eq!(d.changes, bundle.changes);
862
        assert_eq!(d.attests, bundle.attests);
863
        assert_eq!(d.objects, bundle.objects);
864
        assert_eq!(d.policies, bundle.policies);
865
866
        let att = Attest {
867
            repo: [3; 16],
868
            hlc: Hlc::default(),
869
            body: AttestBody::Ref {
870
                name: "main".into(),
871
                old_state: ObjectRef([1; 32]),
872
                new_state: ObjectRef([2; 32]),
873
                serial: 4,
874
            },
875
        };
876
        let refs = vec![
877
            RemoteRef {
878
                name: "main".into(),
879
                state: ObjectRef([2; 32]),
880
                heads: vec![c.hash()],
881
                attestation: Some(SignedAttest {
882
                    attest: att.clone(),
883
                    sigs: vec![sig.clone()],
884
                }),
885
                last_serial: Some(4),
886
            },
887
            RemoteRef {
888
                name: "dev".into(),
889
                state: ObjectRef([5; 32]),
890
                heads: vec![],
891
                attestation: None,
892
                last_serial: None,
893
            },
894
        ];
895
        let tag = TagRef {
896
            name: "v1".into(),
897
            state: ObjectRef([2; 32]),
898
            heads: vec![c.hash()],
899
            attestation: None,
900
        };
901
        let resp = RefsResponse {
902
            refs,
903
            policy: Some(SignedPolicy {
904
                policy: policy.clone(),
905
                sigs: vec![psig.clone()],
906
            }),
907
            tags: vec![tag.clone()],
908
            history: vec![SignedPolicy {
909
                policy: policy.clone(),
910
                sigs: vec![],
911
            }],
912
        };
913
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &resp)).unwrap();
914
        assert_eq!(id_back, [3; 16]);
915
        assert_eq!(back.refs.len(), 2);
916
        assert_eq!(back.refs[0].attestation.as_ref().unwrap().attest, att);
917
        assert_eq!(back.refs[1].last_serial, None);
918
        assert_eq!(back.policy.as_ref().unwrap().policy, policy);
919
        assert_eq!(back.history, resp.history);
920
        assert_eq!(back.tags, vec![tag.clone()]);
921
922
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(
923
            &[ObjectRef([1; 32])],
924
            &[ObjectRef([2; 32])],
925
        ))
926
        .unwrap();
927
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
928
        assert_eq!(
929
            wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(),
930
            vec![c.hash()]
931
        );
932
        assert_eq!(
933
            wire::decode_fetch(&wire::encode_fetch(&[c.hash()], Some(4096))).unwrap(),
934
            (vec![c.hash()], Some(4096))
935
        );
936
        assert_eq!(
937
            wire::decode_fetch(&wire::encode_fetch(&[], None)).unwrap(),
938
            (vec![], None)
939
        );
940
        assert_eq!(
941
            wire::decode_shallow(&wire::encode_shallow(&[c.hash()], 3)).unwrap(),
942
            (vec![c.hash()], 3)
943
        );
944
        // An old-style fetch request without the filter field still decodes.
945
        assert_eq!(
946
            wire::decode_fetch(&wire::encode_hashes(&[c.hash()])).unwrap(),
947
            (vec![c.hash()], None)
948
        );
949
950
        let up = RefUpdate {
951
            name: "main".into(),
952
            old_state: ObjectRef([1; 32]),
953
            new_state: ObjectRef([2; 32]),
954
            force: true,
955
            attestation: Some(SignedAttest {
956
                attest: att,
957
                sigs: vec![sig],
958
            }),
959
        };
960
        let sp = SignedPolicy {
961
            policy,
962
            sigs: vec![psig],
963
        };
964
        let (b2, ups, pol, tags) = wire::decode_push(&wire::encode_push(
965
            &bundle,
966
            std::slice::from_ref(&up),
967
            Some(&sp),
968
            std::slice::from_ref(&tag),
969
        ))
970
        .unwrap();
971
        assert_eq!(pol, Some(sp));
972
        assert_eq!(tags, vec![tag]);
973
        assert_eq!(b2.changes.len(), 1);
974
        assert_eq!(ups[0].name, "main");
975
        assert!(ups[0].force);
976
        assert_eq!(ups[0].attestation, up.attestation);
977
        let res = vec![RefResult {
978
            name: "main".into(),
979
            ok: false,
980
            reason: "stale".into(),
981
        }];
982
        assert_eq!(
983
            wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(),
984
            res
985
        );
986
        assert_eq!(
987
            wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(),
988
            [7; 16]
989
        );
990
    }
991
}