OXYGENHUB
# 33e77a5526fa oxygen / crates / oxy-core / src / protocol.rs
37.4 KiB · Rust · last changed oxy doctor, and failures that say what to do next 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
                let msg = format!("{url}: HTTP {code}: {}", text.trim());
308
                Err(RepoError::Msg(with_hint(msg)))
309
            }
310
            // The transport error already names the URL.
311
            Err(e) => Err(RepoError::Msg(with_hint(e.to_string()))),
312
        }
313
    }
314
}
315
316
/// What to do about a failed request, recognised from its message.
317
pub fn hint_for(message: &str) -> Option<&'static str> {
318
    let m = message.to_ascii_lowercase();
319
    let hint = if m.contains("http 401") {
320
        "the server did not accept the token: set one with `oxy config remote.<remote>.token TOKEN` (a personal access token; pushing needs repo:write)"
321
    } else if m.contains("http 403") {
322
        "the token or account lacks permission: pushing needs a token with repo:write and write access to the repository"
323
    } else if m.contains("http 404") {
324
        "nothing at this URL: check the owner and repository name, or create the repository on the hub first"
325
    } else if m.contains("unknownissuer")
326
        || m.contains("unknown issuer")
327
        || m.contains("certificate")
328
        || m.contains("invalidcertificate")
329
    {
330
        "the server's certificate is not trusted: for a hub with a self-signed certificate, download it from /.well-known/oxyhub-ca.pem on the hub and run `oxy config remote.<remote>.ca FILE`"
331
    } else if m.contains("connection refused") {
332
        "nothing is listening there: check the URL and port, and that the server is running"
333
    } else if m.contains("dns")
334
        || m.contains("failed to lookup")
335
        || m.contains("name or service not known")
336
    {
337
        "the host name does not resolve: check the URL"
338
    } else if m.contains("timed out") || m.contains("timeout") {
339
        "the server did not answer in time: check the network, or try again"
340
    } else {
341
        return None;
342
    };
343
    Some(hint)
344
}
345
346
/// `message`, followed by what to do about it when that is known.
347
pub fn with_hint(message: String) -> String {
348
    match hint_for(&message) {
349
        Some(h) => format!("{message}\n  hint: {h}"),
350
        None => message,
351
    }
352
}
353
354
/// An authenticated GET of any URL on a host, with the same TLS trust and
355
/// token as remotes use (for hub APIs: proposals). Returns the body.
356
pub fn http_get(url: &str, options: &RemoteOptions) -> Result<Vec<u8>> {
357
    let agent = ureq::AgentBuilder::new()
358
        .tls_config(tls_client_config(options.ca.as_deref())?)
359
        .build();
360
    let mut req = agent.get(url).set("Accept", "application/json");
361
    if let Some(t) = &options.token {
362
        req = req.set("Authorization", &format!("Bearer {t}"));
363
    }
364
    match req.call() {
365
        Ok(r) => {
366
            let mut out = Vec::new();
367
            r.into_reader()
368
                .read_to_end(&mut out)
369
                .map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
370
            Ok(out)
371
        }
372
        Err(ureq::Error::Status(code, r)) => {
373
            let text = r.into_string().unwrap_or_default();
374
            Err(RepoError::Msg(with_hint(format!(
375
                "{url}: HTTP {code}: {}",
376
                text.trim()
377
            ))))
378
        }
379
        // The transport error already names the URL.
380
        Err(e) => Err(RepoError::Msg(with_hint(e.to_string()))),
381
    }
382
}
383
384
impl Remote for HttpRemote {
385
    fn repo_id(&self) -> Result<[u8; 16]> {
386
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
387
    }
388
    fn refs(&self) -> Result<RefsResponse> {
389
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
390
    }
391
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
392
        wire::decode_hashes(&self.call(
393
            true,
394
            "negotiate",
395
            Some(&wire::encode_negotiate(have, want)),
396
        )?)
397
    }
398
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
399
        wire::decode_bundle(&self.call(
400
            true,
401
            "fetch",
402
            Some(&wire::encode_fetch(changes, max_blob)),
403
        )?)
404
    }
405
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
406
        Ok(
407
            wire::decode_bundle(&self.call(true, "objects", Some(&wire::encode_hashes(refs)))?)?
408
                .objects,
409
        )
410
    }
411
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
412
        wire::decode_bundle(&self.call(true, "checkpoint", Some(&wire::encode_hashes(heads)))?)
413
    }
414
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
415
        wire::decode_hashes(&self.call(
416
            true,
417
            "shallow",
418
            Some(&wire::encode_shallow(heads, depth)),
419
        )?)
420
    }
421
    fn push(
422
        &self,
423
        bundle: Bundle,
424
        updates: Vec<RefUpdate>,
425
        policy: Option<SignedPolicy>,
426
        tags: Vec<TagRef>,
427
    ) -> Result<Vec<RefResult>> {
428
        wire::decode_push_response(&self.call(
429
            true,
430
            "push",
431
            Some(&wire::encode_push(
432
                &bundle,
433
                &updates,
434
                policy.as_ref(),
435
                &tags,
436
            )),
437
        )?)
438
    }
439
}
440
441
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
442
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
443
    if url.starts_with("http://") || url.starts_with("https://") {
444
        return Ok(Box::new(HttpRemote::new(url, options)?));
445
    }
446
    let path = url.strip_prefix("file://").unwrap_or(url);
447
    if path.contains("://") {
448
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
449
    }
450
    let mut local = LocalRemote::open(Path::new(path))?;
451
    local.claimed_id = options.repo_id;
452
    Ok(Box::new(local))
453
}
454
455
// ---------------------------------------------------------------- wire
456
457
/// CBOR encodings of the protocol messages (spec §16.1).
458
pub mod wire {
459
    use super::*;
460
461
    fn schema(msg: &str) -> RepoError {
462
        RepoError::Object(ObjectError::Schema(msg.into()))
463
    }
464
    fn sigs_value(sigs: &[Signature]) -> Value {
465
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
466
    }
467
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
468
        Ok(o::array(v, "signatures")?
469
            .iter()
470
            .map(Signature::from_value)
471
            .collect::<std::result::Result<Vec<_>, _>>()?)
472
    }
473
    fn hashes_value(hs: &[ObjectRef]) -> Value {
474
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
475
    }
476
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
477
        Ok(o::array(v, "hashes")?
478
            .iter()
479
            .map(|h| o::oref(h, "hash"))
480
            .collect::<std::result::Result<Vec<_>, _>>()?)
481
    }
482
    fn attest_value(sa: &SignedAttest) -> Value {
483
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
484
    }
485
    fn attest_from(v: &Value) -> Result<SignedAttest> {
486
        let a = o::array(v, "signed attest")?;
487
        if a.len() != 2 {
488
            return Err(schema("signed attest: expected [attest, sigs]"));
489
        }
490
        Ok(SignedAttest {
491
            attest: Attest::decode(o::bytes(&a[0], "attest")?)?,
492
            sigs: sigs_from(&a[1])?,
493
        })
494
    }
495
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
496
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
497
    }
498
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
499
        if v.is_null() {
500
            Ok(None)
501
        } else {
502
            attest_from(v).map(Some)
503
        }
504
    }
505
    /// `[policy bytes, [signature]]`: also the format of a policy proposal
506
    /// file exchanged between root holders (spec §14.5).
507
    pub fn policy_value(sp: &SignedPolicy) -> Value {
508
        Value::Array(vec![Value::Bytes(sp.policy.encode()), sigs_value(&sp.sigs)])
509
    }
510
    pub fn policy_from(v: &Value) -> Result<SignedPolicy> {
511
        let a = o::array(v, "signed policy")?;
512
        if a.len() != 2 {
513
            return Err(schema("signed policy: expected [policy, sigs]"));
514
        }
515
        Ok(SignedPolicy {
516
            policy: Policy::decode(o::bytes(&a[0], "policy")?)?,
517
            sigs: sigs_from(&a[1])?,
518
        })
519
    }
520
    fn opt_policy_value(sp: Option<&SignedPolicy>) -> Value {
521
        sp.map(policy_value).unwrap_or(Value::Null)
522
    }
523
    fn opt_policy_from(v: &Value) -> Result<Option<SignedPolicy>> {
524
        if v.is_null() {
525
            Ok(None)
526
        } else {
527
            policy_from(v).map(Some)
528
        }
529
    }
530
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
531
        Ok(o::map(v, what)?)
532
    }
533
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
534
        Ok(o::field(m, k, what)?)
535
    }
536
537
    pub fn bundle_value(b: &Bundle) -> Value {
538
        Value::map([
539
            (
540
                0,
541
                Value::Array(
542
                    b.changes
543
                        .iter()
544
                        .map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)]))
545
                        .collect(),
546
                ),
547
            ),
548
            (
549
                1,
550
                Value::Array(b.attests.iter().map(attest_value).collect()),
551
            ),
552
            (
553
                2,
554
                Value::Array(
555
                    b.objects
556
                        .iter()
557
                        .map(|(t, body)| {
558
                            Value::Array(vec![
559
                                Value::Text(t.name().into()),
560
                                Value::Bytes(body.clone()),
561
                            ])
562
                        })
563
                        .collect(),
564
                ),
565
            ),
566
            (
567
                3,
568
                Value::Array(b.policies.iter().map(policy_value).collect()),
569
            ),
570
        ])
571
    }
572
573
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
574
        let m = map(v, "bundle")?;
575
        let mut b = Bundle::default();
576
        for c in o::array(field(m, 0, "changes")?, "changes")? {
577
            let pair = o::array(c, "change entry")?;
578
            if pair.len() != 2 {
579
                return Err(schema("change entry: expected [change, sigs]"));
580
            }
581
            b.changes.push((
582
                Change::decode(o::bytes(&pair[0], "change")?)?,
583
                sigs_from(&pair[1])?,
584
            ));
585
        }
586
        for a in o::array(field(m, 1, "attests")?, "attests")? {
587
            b.attests.push(attest_from(a)?);
588
        }
589
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
590
            let pair = o::array(ob, "object")?;
591
            if pair.len() != 2 {
592
                return Err(schema("object: expected [type, body]"));
593
            }
594
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?)
595
                .ok_or_else(|| schema("unknown object type"))?;
596
            b.objects
597
                .push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
598
        }
599
        if let Some(ps) = m.get(&3) {
600
            for p in o::array(ps, "policies")? {
601
                b.policies.push(policy_from(p)?);
602
            }
603
        }
604
        Ok(b)
605
    }
606
607
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
608
        cbor::encode(&bundle_value(b))
609
    }
610
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
611
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
612
    }
613
614
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
615
        cbor::encode(&Value::map([
616
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
617
            (1, Value::Bytes(id.to_vec())),
618
            (2, Value::Array(vec![Value::Text("seal".into())])),
619
        ]))
620
    }
621
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
622
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
623
        let m = map(&v, "capabilities")?;
624
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
625
        if !versions
626
            .iter()
627
            .any(|x| x.as_uint() == Some(PROTOCOL_VERSION))
628
        {
629
            return Err(RepoError::Msg(
630
                "remote does not speak protocol version 1".into(),
631
            ));
632
        }
633
        o::bytes(field(m, 1, "repo id")?, "repo id")?
634
            .try_into()
635
            .map_err(|_| schema("repo id: expected 16 bytes"))
636
    }
637
638
    fn tags_value(tags: &[TagRef]) -> Value {
639
        Value::Array(
640
            tags.iter()
641
                .map(|t| {
642
                    Value::map([
643
                        (0, Value::Text(t.name.clone())),
644
                        (1, Value::Bytes(t.state.0.to_vec())),
645
                        (2, hashes_value(&t.heads)),
646
                        (3, opt_attest_value(&t.attestation)),
647
                    ])
648
                })
649
                .collect(),
650
        )
651
    }
652
    fn tags_from(v: &Value) -> Result<Vec<TagRef>> {
653
        let mut out = Vec::new();
654
        for t in o::array(v, "tags")? {
655
            let tm = map(t, "tag")?;
656
            out.push(TagRef {
657
                name: o::text(field(tm, 0, "name")?, "name")?.to_owned(),
658
                state: o::oref(field(tm, 1, "state")?, "state")?,
659
                heads: hashes_from(field(tm, 2, "heads")?)?,
660
                attestation: opt_attest_from(field(tm, 3, "attestation")?)?,
661
            });
662
        }
663
        Ok(out)
664
    }
665
666
    pub fn encode_refs(id: [u8; 16], resp: &RefsResponse) -> Vec<u8> {
667
        cbor::encode(&Value::map([
668
            (
669
                0,
670
                Value::Array(
671
                    resp.refs
672
                        .iter()
673
                        .map(|r| {
674
                            Value::map([
675
                                (0, Value::Text(r.name.clone())),
676
                                (1, Value::Bytes(r.state.0.to_vec())),
677
                                (2, hashes_value(&r.heads)),
678
                                (3, opt_attest_value(&r.attestation)),
679
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
680
                            ])
681
                        })
682
                        .collect(),
683
                ),
684
            ),
685
            (1, Value::Bytes(id.to_vec())),
686
            (2, opt_policy_value(resp.policy.as_ref())),
687
            (3, tags_value(&resp.tags)),
688
            (
689
                4,
690
                Value::Array(resp.history.iter().map(policy_value).collect()),
691
            ),
692
        ]))
693
    }
694
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], RefsResponse)> {
695
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
696
        let m = map(&v, "refs")?;
697
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?
698
            .try_into()
699
            .map_err(|_| schema("repo id: expected 16 bytes"))?;
700
        let mut refs = Vec::new();
701
        for r in o::array(field(m, 0, "refs")?, "refs")? {
702
            let rm = map(r, "ref")?;
703
            let ls = field(rm, 4, "last serial")?;
704
            refs.push(RemoteRef {
705
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
706
                state: o::oref(field(rm, 1, "state")?, "state")?,
707
                heads: hashes_from(field(rm, 2, "heads")?)?,
708
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
709
                last_serial: if ls.is_null() {
710
                    None
711
                } else {
712
                    Some(o::uint(ls, "last serial")?)
713
                },
714
            });
715
        }
716
        let policy = match m.get(&2) {
717
            Some(p) => opt_policy_from(p)?,
718
            None => None,
719
        };
720
        let tags = match m.get(&3) {
721
            Some(t) => tags_from(t)?,
722
            None => Vec::new(),
723
        };
724
        let mut history = Vec::new();
725
        if let Some(h) = m.get(&4) {
726
            for p in o::array(h, "history")? {
727
                history.push(policy_from(p)?);
728
            }
729
        }
730
        Ok((
731
            id,
732
            RefsResponse {
733
                refs,
734
                policy,
735
                tags,
736
                history,
737
            },
738
        ))
739
    }
740
741
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
742
        cbor::encode(&Value::map([
743
            (0, hashes_value(have)),
744
            (1, hashes_value(want)),
745
        ]))
746
    }
747
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
748
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
749
        let m = map(&v, "negotiate")?;
750
        Ok((
751
            hashes_from(field(m, 0, "have")?)?,
752
            hashes_from(field(m, 1, "want")?)?,
753
        ))
754
    }
755
756
    /// `fetch` request: `{0: [changes], 1: max blob size or null}`.
757
    pub fn encode_fetch(hs: &[ChangeHash], max_blob: Option<u64>) -> Vec<u8> {
758
        cbor::encode(&Value::map([
759
            (0, hashes_value(hs)),
760
            (1, max_blob.map(Value::Uint).unwrap_or(Value::Null)),
761
        ]))
762
    }
763
    pub fn decode_fetch(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Option<u64>)> {
764
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
765
        let m = map(&v, "fetch")?;
766
        let max = match m.get(&1) {
767
            Some(x) if !x.is_null() => Some(o::uint(x, "max blob")?),
768
            _ => None,
769
        };
770
        Ok((hashes_from(field(m, 0, "changes")?)?, max))
771
    }
772
773
    /// `shallow` request: `{0: [heads], 1: depth}`.
774
    pub fn encode_shallow(hs: &[ChangeHash], depth: u32) -> Vec<u8> {
775
        cbor::encode(&Value::map([
776
            (0, hashes_value(hs)),
777
            (1, Value::Uint(depth as u64)),
778
        ]))
779
    }
780
    pub fn decode_shallow(bytes: &[u8]) -> Result<(Vec<ChangeHash>, u32)> {
781
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
782
        let m = map(&v, "shallow")?;
783
        Ok((
784
            hashes_from(field(m, 0, "heads")?)?,
785
            o::u32_(field(m, 1, "depth")?, "depth")?,
786
        ))
787
    }
788
789
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
790
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
791
    }
792
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
793
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
794
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
795
    }
796
797
    pub fn encode_push(
798
        bundle: &Bundle,
799
        updates: &[RefUpdate],
800
        policy: Option<&SignedPolicy>,
801
        tags: &[TagRef],
802
    ) -> Vec<u8> {
803
        cbor::encode(&Value::map([
804
            (0, bundle_value(bundle)),
805
            (2, opt_policy_value(policy)),
806
            (3, tags_value(tags)),
807
            (
808
                1,
809
                Value::Array(
810
                    updates
811
                        .iter()
812
                        .map(|u| {
813
                            Value::map([
814
                                (0, Value::Text(u.name.clone())),
815
                                (1, Value::Bytes(u.old_state.0.to_vec())),
816
                                (2, Value::Bytes(u.new_state.0.to_vec())),
817
                                (3, Value::Bool(u.force)),
818
                                (4, opt_attest_value(&u.attestation)),
819
                            ])
820
                        })
821
                        .collect(),
822
                ),
823
            ),
824
        ]))
825
    }
826
    /// A decoded push: bundle, bookmark updates, policy, tags.
827
    pub type PushParts = (Bundle, Vec<RefUpdate>, Option<SignedPolicy>, Vec<TagRef>);
828
829
    pub fn decode_push(bytes: &[u8]) -> Result<PushParts> {
830
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
831
        let m = map(&v, "push")?;
832
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
833
        let mut updates = Vec::new();
834
        for u in o::array(field(m, 1, "updates")?, "updates")? {
835
            let um = map(u, "update")?;
836
            updates.push(RefUpdate {
837
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
838
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
839
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
840
                force: field(um, 3, "force")?
841
                    .as_bool()
842
                    .ok_or_else(|| schema("force: expected bool"))?,
843
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
844
            });
845
        }
846
        let policy = match m.get(&2) {
847
            Some(p) => opt_policy_from(p)?,
848
            None => None,
849
        };
850
        let tags = match m.get(&3) {
851
            Some(t) => tags_from(t)?,
852
            None => Vec::new(),
853
        };
854
        Ok((bundle, updates, policy, tags))
855
    }
856
857
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
858
        cbor::encode(&Value::map([(
859
            0,
860
            Value::Array(
861
                results
862
                    .iter()
863
                    .map(|r| {
864
                        Value::map([
865
                            (0, Value::Text(r.name.clone())),
866
                            (1, Value::Bool(r.ok)),
867
                            (2, Value::Text(r.reason.clone())),
868
                        ])
869
                    })
870
                    .collect(),
871
            ),
872
        )]))
873
    }
874
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
875
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
876
        let m = map(&v, "push response")?;
877
        let mut out = Vec::new();
878
        for r in o::array(field(m, 0, "results")?, "results")? {
879
            let rm = map(r, "result")?;
880
            out.push(RefResult {
881
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
882
                ok: field(rm, 1, "ok")?
883
                    .as_bool()
884
                    .ok_or_else(|| schema("ok: expected bool"))?,
885
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
886
            });
887
        }
888
        Ok(out)
889
    }
890
}
891
892
#[cfg(test)]
893
mod tests {
894
    use super::*;
895
    use crate::attest::AttestBody;
896
    use crate::objects::Hlc;
897
898
    #[test]
899
    fn wire_round_trips() {
900
        let c = crate::objects::tests::sample_change();
901
        let sk = crate::identity::SecretKey {
902
            ed_seed: [1; 32],
903
            ml_seed: [2; 32],
904
        };
905
        let id = sk.identity("a", "b", 0);
906
        let key = id.hash();
907
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
908
        let seal = Attest::seal([3; 16], vec![c.hash()]);
909
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
910
        let policy = Policy::single_root([3; 16], id.clone(), 99);
911
        let psig = sk.sign(crate::identity::Domain::Policy, &key, &policy.hash());
912
        let bundle = Bundle {
913
            changes: vec![(c.clone(), vec![sig.clone()])],
914
            attests: vec![SignedAttest {
915
                attest: seal.clone(),
916
                sigs: vec![seal_sig],
917
            }],
918
            objects: vec![
919
                (ObjectType::Identity, id.encode()),
920
                (ObjectType::Chunk, b"xyz".to_vec()),
921
            ],
922
            policies: vec![SignedPolicy {
923
                policy: policy.clone(),
924
                sigs: vec![psig.clone()],
925
            }],
926
        };
927
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
928
        assert_eq!(d.changes, bundle.changes);
929
        assert_eq!(d.attests, bundle.attests);
930
        assert_eq!(d.objects, bundle.objects);
931
        assert_eq!(d.policies, bundle.policies);
932
933
        let att = Attest {
934
            repo: [3; 16],
935
            hlc: Hlc::default(),
936
            body: AttestBody::Ref {
937
                name: "main".into(),
938
                old_state: ObjectRef([1; 32]),
939
                new_state: ObjectRef([2; 32]),
940
                serial: 4,
941
            },
942
        };
943
        let refs = vec![
944
            RemoteRef {
945
                name: "main".into(),
946
                state: ObjectRef([2; 32]),
947
                heads: vec![c.hash()],
948
                attestation: Some(SignedAttest {
949
                    attest: att.clone(),
950
                    sigs: vec![sig.clone()],
951
                }),
952
                last_serial: Some(4),
953
            },
954
            RemoteRef {
955
                name: "dev".into(),
956
                state: ObjectRef([5; 32]),
957
                heads: vec![],
958
                attestation: None,
959
                last_serial: None,
960
            },
961
        ];
962
        let tag = TagRef {
963
            name: "v1".into(),
964
            state: ObjectRef([2; 32]),
965
            heads: vec![c.hash()],
966
            attestation: None,
967
        };
968
        let resp = RefsResponse {
969
            refs,
970
            policy: Some(SignedPolicy {
971
                policy: policy.clone(),
972
                sigs: vec![psig.clone()],
973
            }),
974
            tags: vec![tag.clone()],
975
            history: vec![SignedPolicy {
976
                policy: policy.clone(),
977
                sigs: vec![],
978
            }],
979
        };
980
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &resp)).unwrap();
981
        assert_eq!(id_back, [3; 16]);
982
        assert_eq!(back.refs.len(), 2);
983
        assert_eq!(back.refs[0].attestation.as_ref().unwrap().attest, att);
984
        assert_eq!(back.refs[1].last_serial, None);
985
        assert_eq!(back.policy.as_ref().unwrap().policy, policy);
986
        assert_eq!(back.history, resp.history);
987
        assert_eq!(back.tags, vec![tag.clone()]);
988
989
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(
990
            &[ObjectRef([1; 32])],
991
            &[ObjectRef([2; 32])],
992
        ))
993
        .unwrap();
994
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
995
        assert_eq!(
996
            wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(),
997
            vec![c.hash()]
998
        );
999
        assert_eq!(
1000
            wire::decode_fetch(&wire::encode_fetch(&[c.hash()], Some(4096))).unwrap(),
1001
            (vec![c.hash()], Some(4096))
1002
        );
1003
        assert_eq!(
1004
            wire::decode_fetch(&wire::encode_fetch(&[], None)).unwrap(),
1005
            (vec![], None)
1006
        );
1007
        assert_eq!(
1008
            wire::decode_shallow(&wire::encode_shallow(&[c.hash()], 3)).unwrap(),
1009
            (vec![c.hash()], 3)
1010
        );
1011
        // An old-style fetch request without the filter field still decodes.
1012
        assert_eq!(
1013
            wire::decode_fetch(&wire::encode_hashes(&[c.hash()])).unwrap(),
1014
            (vec![c.hash()], None)
1015
        );
1016
1017
        let up = RefUpdate {
1018
            name: "main".into(),
1019
            old_state: ObjectRef([1; 32]),
1020
            new_state: ObjectRef([2; 32]),
1021
            force: true,
1022
            attestation: Some(SignedAttest {
1023
                attest: att,
1024
                sigs: vec![sig],
1025
            }),
1026
        };
1027
        let sp = SignedPolicy {
1028
            policy,
1029
            sigs: vec![psig],
1030
        };
1031
        let (b2, ups, pol, tags) = wire::decode_push(&wire::encode_push(
1032
            &bundle,
1033
            std::slice::from_ref(&up),
1034
            Some(&sp),
1035
            std::slice::from_ref(&tag),
1036
        ))
1037
        .unwrap();
1038
        assert_eq!(pol, Some(sp));
1039
        assert_eq!(tags, vec![tag]);
1040
        assert_eq!(b2.changes.len(), 1);
1041
        assert_eq!(ups[0].name, "main");
1042
        assert!(ups[0].force);
1043
        assert_eq!(ups[0].attestation, up.attestation);
1044
        let res = vec![RefResult {
1045
            name: "main".into(),
1046
            ok: false,
1047
            reason: "stale".into(),
1048
        }];
1049
        assert_eq!(
1050
            wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(),
1051
            res
1052
        );
1053
        assert_eq!(
1054
            wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(),
1055
            [7; 16]
1056
        );
1057
    }
1058
}