OXYGENHUB

Certified keys may push under a subject grant (keyless step 4b) f0a517d28237

Bundles carry signed certificates (field 4, only when there are any;
older peers ignore it) for the short-lived keys that signed their changes,
seals or the push's attestation, with the keys' and issuers' identities.
A receiver keeps each that verifies with its issuer and refuses a bundle
with one that does not. A signer is authorized for a reference or a
change when its certificate verifies, gives the scope (refs, changes),
covers the receiver's own clock on a push (the attestation's time on a
fetch until step 4c) and names a subject the policy grants. Revoking a
key no longer drops a rule of subjects alone. Spec 14.5, 16.3. Tests: a
certified agent pushes and a fetch accepts it; an expired certificate and
an ungranted subject are refused; certificates round-trip in a bundle,
which encodes as before without them.

Refs andymccall/oxygenhub#241
Author
Andy McCall @andymccall <[email protected]>
via
agent Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
When
· 2026-10-17 17:31 UTC
Signature
✔ signed by Claude Code
Hash
f0a517d282376d51a83b818987835d0b01e0c63fc120873c8b71304a8f7e1308
Depends on
096f447ccda0 28fc23d7706a 2c75672fb2fe 3526d339519f 3a6effc74835 5b9c6ad45f6e 68a924846379 6eae06b6ec04 6f8aeb996558 750251b21a96 75167672ebcd 7ba155f20aa0 83e0677b07fe 86e682c242c6 982864068a59 f8c695764c95 ff0af1ee299d
Depended on by
45a6f5ec85cf f0ef9778e587 1ec6fdbc6bcb d8524a9f0117 d36ba2f79f95 028545dfa7a4 a2387d547373 f53cbe7a204e
In bookmarks
carried-not-removed doctor-trust identical-sides keyless-tags main merge-remote-bookmark paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state
In tags
v0.10.0 v0.11.0 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
26 atoms, 0 tree operations, 5 files

Browse the tree at this change

modified crates/oxy-core/src/protocol.rs +57 −2
…
95 unchanged lines
96 96
    /// fetch between repositories on one machine: their text blobs and
97 97
    /// chunks are among `objects`. Never on the wire.
98 98
    pub raw_changes: Vec<(Vec<u8>, Vec<Signature>)>,
99 +
    /// Certificates of short-lived keys that signed what the bundle carries
100 +
    /// (spec §14.7), their issuers' identities among `objects`.
101 +
    pub certificates: Vec<crate::cert::SignedCertificate>,
99 102
}
100 103
101 104
impl Bundle {
…
629 unchanged lines
731 734
            }
732 735
            changes.push(Value::Array(vec![Value::Bytes(bytes), sigs_value(s)]));
733 736
        }
734 -
        Value::map([
737 +
        let mut v = Value::map([
735 738
            (0, Value::Array(changes)),
736 739
            (
737 740
                1,
…
18 unchanged lines
756 759
                3,
757 760
                Value::Array(b.policies.iter().map(policy_value).collect()),
758 761
            ),
759 -
        ])
762 +
        ]);
763 +
        // Only when there are any; older peers ignore the field.
764 +
        if !b.certificates.is_empty() {
765 +
            if let Value::Map(m) = &mut v {
766 +
                m.insert(
767 +
                    4,
768 +
                    Value::Array(
769 +
                        b.certificates
770 +
                            .iter()
771 +
                            .map(|c| Value::Bytes(c.encode()))
772 +
                            .collect(),
773 +
                    ),
774 +
                );
775 +
            }
776 +
        }
777 +
        v
760 778
    }
761 779
762 780
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
…
51 unchanged lines
814 832
        if let Some(ps) = m.get(&3) {
815 833
            for p in o::array(ps, "policies")? {
816 834
                b.policies.push(policy_from(p)?);
835 +
            }
836 +
        }
837 +
        if let Some(cs) = m.get(&4) {
838 +
            for c in o::array(cs, "certificates")? {
839 +
                b.certificates.push(
840 +
                    crate::cert::SignedCertificate::decode(o::bytes(c, "certificate")?)
841 +
                        .map_err(|e| schema(&format!("certificate: {e}")))?,
842 +
                );
817 843
            }
818 844
        }
819 845
        Ok(b)
…
332 unchanged lines
1152 1178
            }],
1153 1179
            hashes: vec![],
1154 1180
            raw_changes: vec![],
1181 +
            certificates: vec![],
1155 1182
        };
1156 1183
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
1157 1184
        assert_eq!(
…
5 unchanged lines
1163 1190
        assert_eq!(d.attests, bundle.attests);
1164 1191
        assert_eq!(d.objects, bundle.objects);
1165 1192
        assert_eq!(d.policies, bundle.policies);
1193 +
        assert!(d.certificates.is_empty());
1194 +
        // Certificates (keyless, oxygenhub#241) travel as field 4, only
1195 +
        // when there are any: a bundle without them encodes as before.
1196 +
        let plain = wire::encode_bundle(&bundle);
1197 +
        let mut with = bundle.clone();
1198 +
        let issuer_sk = crate::identity::SecretKey::generate();
1199 +
        let issuer = issuer_sk.identity("hub", "h@x", 1);
1200 +
        with.certificates.push(crate::cert::SignedCertificate::sign(
1201 +
            crate::cert::Certificate {
1202 +
                key: id.clone(),
1203 +
                issuer: issuer.hash(),
1204 +
                subject: "agent:a@b".into(),
1205 +
                claims: vec![],
1206 +
                scope: crate::identity::Scope::CHANGES,
1207 +
                not_before: 1,
1208 +
                expires: 2,
1209 +
            },
1210 +
            &issuer,
1211 +
            &issuer_sk,
1212 +
        ));
1213 +
        let encoded = wire::encode_bundle(&with);
1214 +
        assert_ne!(encoded, plain);
1215 +
        assert_eq!(
1216 +
            wire::decode_bundle(&encoded).unwrap().certificates,
1217 +
            with.certificates
1218 +
        );
1219 +
        with.certificates.clear();
1220 +
        assert_eq!(wire::encode_bundle(&with), plain);
1166 1221
1167 1222
        let att = Attest {
1168 1223
            repo: [3; 16],
…
125 unchanged lines
modified crates/oxy-core/src/repo/policy.rs +13 −2
…
409 unchanged lines
410 410
        for r in &mut p.rules {
411 411
            r.keys.retain(|k| k != key);
412 412
        }
413 -
        p.rules.retain(|r| !r.keys.is_empty());
413 +
        // A rule of certified subjects alone stays.
414 +
        p.rules
415 +
            .retain(|r| !r.keys.is_empty() || !r.subjects.is_empty());
414 416
        if !p.revoked.contains(key) {
415 417
            p.revoked.push(*key);
416 418
            p.revoked.sort();
…
30 unchanged lines
447 449
                    .map(|c| c.author)
448 450
                    .map_err(|e| e.to_string())?;
449 451
                let principal = if author.is_zero() { key } else { author };
452 +
                // A change is checked when it is received: now is a time
453 +
                // its signer did not choose.
450 454
                let covered = rule
451 455
                    .keys
452 456
                    .iter()
453 -
                    .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal));
457 +
                    .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal))
458 +
                    || self.certified_authorized(
459 +
                        policy,
460 +
                        &key,
461 +
                        reference,
462 +
                        crate::identity::Scope::CHANGES,
463 +
                        crate::objects::Hlc::now().wall_ms,
464 +
                    );
454 465
                if !covered {
455 466
                    return Err(format!(
456 467
                        "change {} is signed by {}, which may not update {reference:?}",
…
48 unchanged lines
modified crates/oxy-core/src/repo/sync.rs +129 −0
…
362 unchanged lines
363 363
            let (ty, body) = self.store.get(&r)?;
364 364
            bundle.objects.push((ty, body));
365 365
        }
366 +
        self.attach_certificates(&mut bundle)?;
366 367
        Ok(bundle)
367 368
    }
368 369
370 +
    /// Add the certificates of short-lived keys that signed what `bundle`
371 +
    /// carries, and their issuers' identities, so a receiver can tell who
372 +
    /// they stand for (spec §14.7). `also` names more signers (a reference
373 +
    /// attestation sent beside the bundle).
374 +
    pub(crate) fn attach_certificates_for(
375 +
        &self,
376 +
        bundle: &mut Bundle,
377 +
        also: &[KeyId],
378 +
    ) -> Result<()> {
379 +
        let mut keys: BTreeSet<KeyId> = also.iter().copied().collect();
380 +
        keys.extend(
381 +
            bundle
382 +
                .changes
383 +
                .iter()
384 +
                .flat_map(|(_, s)| s.iter().map(|s| s.key)),
385 +
        );
386 +
        keys.extend(
387 +
            bundle
388 +
                .raw_changes
389 +
                .iter()
390 +
                .flat_map(|(_, s)| s.iter().map(|s| s.key)),
391 +
        );
392 +
        keys.extend(
393 +
            bundle
394 +
                .attests
395 +
                .iter()
396 +
                .flat_map(|a| a.sigs.iter().map(|s| s.key)),
397 +
        );
398 +
        for k in keys {
399 +
            let Some(r) = self.store.certificate_of(&k)? else {
400 +
                continue;
401 +
            };
402 +
            let cert = self.store.get_certificate(&r)?;
403 +
            if bundle
404 +
                .certificates
405 +
                .iter()
406 +
                .any(|c| c.cert.hash() == cert.cert.hash())
407 +
            {
408 +
                continue;
409 +
            }
410 +
            for id in [&k, &cert.cert.issuer] {
411 +
                if let Ok(body) = self.store.get_typed(id, ObjectType::Identity) {
412 +
                    if !bundle
413 +
                        .objects
414 +
                        .iter()
415 +
                        .any(|(t, b)| *t == ObjectType::Identity && *b == body)
416 +
                    {
417 +
                        bundle.objects.push((ObjectType::Identity, body));
418 +
                    }
419 +
                }
420 +
            }
421 +
            bundle.certificates.push(cert);
422 +
        }
423 +
        Ok(())
424 +
    }
425 +
426 +
    fn attach_certificates(&self, bundle: &mut Bundle) -> Result<()> {
427 +
        self.attach_certificates_for(bundle, &[])
428 +
    }
429 +
430 +
    /// Keep the certificates a bundle brings once each verifies with the
431 +
    /// identity of the issuer it names; one that does not refuses the bundle.
432 +
    fn adopt_certificates(&self, certs: &[crate::cert::SignedCertificate]) -> Result<()> {
433 +
        for c in certs {
434 +
            let issuer = self.store.get_identity(&c.cert.issuer).map_err(|_| {
435 +
                RepoError::Msg(format!(
436 +
                    "a certificate names issuer {}, whose identity did not come with it",
437 +
                    c.cert.issuer.short(12)
438 +
                ))
439 +
            })?;
440 +
            c.verify(&issuer)
441 +
                .map_err(|e| RepoError::Msg(format!("a certificate does not verify: {e}")))?;
442 +
            self.store.put_identity(&c.cert.key)?;
443 +
            self.store.put_certificate(c)?;
444 +
        }
445 +
        Ok(())
446 +
    }
447 +
448 +
    /// A signer authorized for `reference` by what an issuer certified it
449 +
    /// as (spec §14.7): a short-lived key with a kept certificate that
450 +
    /// verifies with its issuer, gives `scope`, covers `at_ms` (a time the
451 +
    /// signer did not choose) and names a subject the policy grants.
452 +
    pub(crate) fn certified_authorized(
453 +
        &self,
454 +
        p: &crate::policy::Policy,
455 +
        signer: &KeyId,
456 +
        reference: &str,
457 +
        scope: Scope,
458 +
        at_ms: u64,
459 +
    ) -> bool {
460 +
        let Ok(Some(r)) = self.store.certificate_of(signer) else {
461 +
            return false;
462 +
        };
463 +
        let Ok(c) = self.store.get_certificate(&r) else {
464 +
            return false;
465 +
        };
466 +
        let Some(issuer) = p
467 +
            .identity(&c.cert.issuer)
468 +
            .cloned()
469 +
            .or_else(|| self.store.get_identity(&c.cert.issuer).ok())
470 +
        else {
471 +
            return false;
472 +
        };
473 +
        c.verify(&issuer).is_ok()
474 +
            && c.cert.key_id() == *signer
475 +
            && c.cert.covers(scope, at_ms)
476 +
            && p.subject_grant(reference, &c.cert.issuer, &c.cert.subject, scope)
477 +
    }
478 +
369 479
    /// Store a bundle's contents. With `strict`, every change must
370 480
    /// authenticate (signature or seal by its author or a delegate).
371 481
    pub fn import_bundle(&self, bundle: &Bundle, strict: bool) -> Result<Vec<ChangeHash>> {
…
43 unchanged lines
415 525
            .map(|(ty, body)| (*ty, body.as_slice()))
416 526
            .collect();
417 527
        self.store.put_many(&objects)?;
528 +
        self.adopt_certificates(&bundle.certificates)?;
418 529
        for sa in &bundle.attests {
419 530
            let r = self.store.put_attest(&sa.attest)?;
420 531
            for sig in &sa.sigs {
…
348 unchanged lines
769 880
                .first()
770 881
                .map(|s| s.key)
771 882
                .ok_or("attestation carries no signature")?;
883 +
            // Received now: the receiver's own clock is the trusted time.
884 +
            let now = Hlc::now().wall_ms;
772 885
            if !p.authorizes(&signer, &u.name)
773 886
                && !self.delegate_authorized(p, &signer, &u.name, sa.attest.hlc.wall_ms)
887 +
                && !self.certified_authorized(p, &signer, &u.name, Scope::REFS, now)
774 888
            {
775 889
                return Err(format!(
776 890
                    "key {} may not delete {:?} under this repository's policy",
…
68 unchanged lines
845 959
                .first()
846 960
                .map(|s| s.key)
847 961
                .ok_or("attestation carries no signature")?;
962 +
            // Received now: the receiver's own clock is the trusted time.
963 +
            let now = Hlc::now().wall_ms;
848 964
            if !p.authorizes(&signer, &u.name)
849 965
                && !self.delegate_authorized(p, &signer, &u.name, sa.attest.hlc.wall_ms)
966 +
                && !self.certified_authorized(p, &signer, &u.name, Scope::REFS, now)
850 967
            {
851 968
                return Err(format!(
852 969
                    "key {} may not update {:?} under this repository's policy",
…
402 unchanged lines
1255 1372
                    .first()
1256 1373
                    .map(|s| s.key)
1257 1374
                    .ok_or_else(|| RepoError::Msg("attestation carries no signature".into()))?;
1375 +
                // Until the host countersigns when it accepted the update,
1376 +
                // the attestation's own time is all a fetch has.
1258 1377
                if !p.authorizes(&signer, &r.name)
1259 1378
                    && !self.delegate_authorized(p, &signer, &r.name, sa.attest.hlc.wall_ms)
1379 +
                    && !self.certified_authorized(
1380 +
                        p,
1381 +
                        &signer,
1382 +
                        &r.name,
1383 +
                        Scope::REFS,
1384 +
                        sa.attest.hlc.wall_ms,
1385 +
                    )
1260 1386
                {
1261 1387
                    return fail(format!(
1262 1388
                        "{remote_name}/{}: attested by {}, which the policy does not authorize for this bookmark; a policy owner can grant it with `oxy policy grant`",
…
434 unchanged lines
1697 1823
                    }
1698 1824
                }
1699 1825
            }
1826 +
            // A short-lived signing key's certificate goes with it (§14.7).
1827 +
            let signers: Vec<KeyId> = self.signing_key().into_iter().collect();
1828 +
            self.attach_certificates_for(&mut bundle, &signers)?;
1700 1829
        }
1701 1830
        let serial = attestation.as_ref().and_then(|a| match a.attest.body {
1702 1831
            AttestBody::Ref { serial, .. } => Some(serial),
…
240 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +92 −0
…
985 unchanged lines
986 986
    );
987 987
}
988 988
989 +
/// Keyless step 4 (oxygenhub#241): an agent signing with a short-lived
990 +
/// key whose certificate an issuer gave pushes when the policy grants its
991 +
/// subject, and a fetch accepts what it attested; an expired certificate,
992 +
/// or a subject the policy does not grant, may not move the bookmark.
989 993
#[test]
994 +
fn a_certified_key_pushes_under_a_subject_grant() {
995 +
    use crate::cert::{Certificate, SignedCertificate};
996 +
    use crate::identity::Scope;
997 +
    hermetic();
998 +
    let dir = tempfile::tempdir().unwrap();
999 +
    let root = dir.path();
1000 +
    let _origin = Repo::init(&root.join("origin")).unwrap();
1001 +
    let url = root.join("origin").to_string_lossy().to_string();
1002 +
    let mut a = Repo::clone(&url, &root.join("a"))
1003 +
        .unwrap()
1004 +
        .with_home(root.join("home-a"));
1005 +
    a.create_identity("Ada", "[email protected]", IdentityScope::User)
1006 +
        .unwrap();
1007 +
    write(&root.join("a"), "a.txt", b"a\n");
1008 +
    a.commit("a", &[]).unwrap();
1009 +
    a.policy_init().unwrap();
1010 +
    assert!(a.push("origin", false).unwrap().iter().all(|r| r.ok));
1011 +
    let hub_sk = SecretKey::generate();
1012 +
    let hub = hub_sk.identity("hub", "[email protected]", 1);
1013 +
    a.policy_grant_subject(
1014 +
        "agent:claude@ada",
1015 +
        hub.clone(),
1016 +
        Scope::CHANGES | Scope::REFS,
1017 +
        None,
1018 +
    )
1019 +
    .unwrap();
1020 +
    assert!(a
1021 +
        .push("origin", false)
1022 +
        .unwrap()
1023 +
        .iter()
1024 +
        .any(|r| r.name == "policy" && r.ok));
1025 +
1026 +
    let now = Hlc::now().wall_ms;
1027 +
    // An agent repository whose key the hub certified for `subject`, from
1028 +
    // `from` to `to`.
1029 +
    let agent = |name: &str, subject: &str, from: u64, to: u64| {
1030 +
        let mut r = Repo::clone(&url, &root.join(name))
1031 +
            .unwrap()
1032 +
            .with_home(root.join(format!("home-{name}")));
1033 +
        let (_, id) = r
1034 +
            .create_identity(name, "[email protected]", IdentityScope::User)
1035 +
            .unwrap();
1036 +
        let cert = SignedCertificate::sign(
1037 +
            Certificate {
1038 +
                key: id,
1039 +
                issuer: hub.hash(),
1040 +
                subject: subject.into(),
1041 +
                claims: vec![],
1042 +
                scope: Scope::CHANGES | Scope::REFS,
1043 +
                not_before: from,
1044 +
                expires: to,
1045 +
            },
1046 +
            &hub,
1047 +
            &hub_sk,
1048 +
        );
1049 +
        r.store().put_identity(&hub).unwrap();
1050 +
        r.store().put_certificate(&cert).unwrap();
1051 +
        write(&root.join(name), &format!("{name}.txt"), b"agent work\n");
1052 +
        r.commit("work", &[]).unwrap();
1053 +
        r
1054 +
    };
1055 +
1056 +
    let claude = agent("claude", "agent:claude@ada", now - 60_000, now + 600_000);
1057 +
    let r = claude.push("origin", false).unwrap();
1058 +
    assert!(r.iter().all(|x| x.ok), "{r:?}");
1059 +
    // Ada's fetch accepts the bookmark the certified key attested.
1060 +
    a.pull("origin").unwrap();
1061 +
    assert!(a.bookmark_heads().unwrap().iter().any(|h| a
1062 +
        .store()
1063 +
        .get_change(h)
1064 +
        .map(|c| c.message == "work")
1065 +
        .unwrap_or(false)));
1066 +
1067 +
    let late = agent("late", "agent:claude@ada", now - 1_200_000, now - 600_000);
1068 +
    let r = late.push("origin", false).unwrap();
1069 +
    assert!(
1070 +
        !r[0].ok && r[0].reason.contains("may not update"),
1071 +
        "an expired certificate: {r:?}"
1072 +
    );
1073 +
    let other = agent("other", "agent:mallory@ada", now - 60_000, now + 600_000);
1074 +
    let r = other.push("origin", false).unwrap();
1075 +
    assert!(
1076 +
        !r[0].ok && r[0].reason.contains("may not update"),
1077 +
        "a subject not granted: {r:?}"
1078 +
    );
1079 +
}
1080 +
1081 +
#[test]
990 1082
fn trust_policy_governs_pushes_and_fetches() {
991 1083
    hermetic();
992 1084
    let dir = tempfile::tempdir().unwrap();
…
5979 unchanged lines
modified docs/spec.md +2 −2
…
673 unchanged lines
674 674
- The initial policy is signed by the root keys; every update MUST be signed by `roots.threshold` root keys of the policy it replaces and have a higher `serial`. An update that a single holder cannot complete is a **proposal**: the signed policy (`[policy bytes, [Signature]]`, §16.3) is passed to other root holders, each of whom adds a signature with `oxy policy sign`; whoever completes the threshold applies it. Rotating a root key (§14.2) substitutes the successor in the roots and rules; such a policy is signed by both the previous and the successor key so a fresh clone can bootstrap from it.
675 675
- A change's signer is authorized by a rule if the rule lists it, or lists a successor of it, or lists the change's `author` (or a successor) when the signer is that author's delegate.
676 676
- A reference attestation's signer is authorized by a rule if the rule lists it, or lists an identity whose `delegates` name it with the `refs` scope, not expired at the attestation's `hlc.wall_ms` (§14.2). Delegation is the identity's own signed statement and the policy names that identity, so a delegate with `refs` moves references exactly as far as its delegator may; revoking either key, or the delegation expiring, ends it. Hosts use this to land and to commit browser edits on a user's behalf with their own key. A delegate without `refs` (an agent) is never authorized for a reference or tag, even when its delegator is.
677 -
- A rule may also grant **subjects an issuer certifies** (§14.7): `oxy policy grant-subject SUBJECT --issuer IDENTITY --scope SCOPES` adds `{pattern, issuer, scope}` and embeds the issuer's identity. A signer is then authorized when it is a short-lived key whose certificate verifies with that issuer, names a subject the pattern matches, gives the scope needed (`changes` for changes, `refs` for reference attestations and tags) and covers a time the signer did not choose; it counts as one signer toward the threshold. A rule with subjects only has no keys, and its threshold counts keys and subjects together. A policy without subjects encodes as before (field 5 is left out), so its hash is unchanged.
677 +
- A rule may also grant **subjects an issuer certifies** (§14.7): `oxy policy grant-subject SUBJECT --issuer IDENTITY --scope SCOPES` adds `{pattern, issuer, scope}` and embeds the issuer's identity. A signer is then authorized when it is a short-lived key whose certificate verifies with that issuer, names a subject the pattern matches, gives the scope needed (`changes` for changes, `refs` for reference attestations and tags) and covers a time the signer did not choose; it counts as one signer toward the threshold. A rule with subjects only has no keys, and its threshold counts keys and subjects together. The time is the receiver's own clock when it receives a push (changes and the reference attestation), and, until a host countersigns when it accepted an update (oxygenhub docs/keyless.md, step 4c), the attestation's own time on a fetch, which is weaker: a key whose certificate expired could backdate an attestation within its scope. A policy without subjects encodes as before (field 5 is left out), so its hash is unchanged.
678 678
- A change or attestation is **authorized** for reference `r` if its signer is listed in the first rule whose pattern matches `r`, and the rule's threshold of distinct authorized signers is met across the attestation and each change in the closure of `new_state` not previously in the closure of `old_state`. `require` selects which authenticators count for changes: individual signatures only, seals only, or either. Every change MUST additionally satisfy the author-or-delegate rule of §14.4 regardless of `require`.
679 679
- Clients MUST verify policy on fetch, MUST refuse to update a `remotes/` reference whose attestation is unauthorized, and MUST warn loudly on an expired policy. Servers MUST enforce the same rules on push.
680 680
- A repository without a `policy` reference is **unverified**; every command that talks to a remote MUST print a one-line warning until one is created. `oxy policy init` creates one with the author's key as the sole root; `oxy policy grant` and `oxy policy revoke` produce successors.
…
82 unchanged lines
763 763
| Message | Encoding |
764 764
|---|---|
765 765
| Signed attestation | `[attest bytes, [Signature]]` |
766 -
| `Bundle` | `{0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}`; changes in dependency order; objects are chunks, blobs, identities, and states only, and include the text blobs of every change in the bundle and their chunks, so a bundle can be read on its own; a push that moves a bookmark also carries the identity that signed the move and, for a delegate, the identity it acts for, so a receiver that has seen no change by that key can check it (oxygenhub#129); key 3 (optional) holds the policies between the receiver's serial and the one sent with a push, ascending |
766 +
| `Bundle` | `{0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}`; changes in dependency order; objects are chunks, blobs, identities, and states only, and include the text blobs of every change in the bundle and their chunks, so a bundle can be read on its own; a push that moves a bookmark also carries the identity that signed the move and, for a delegate, the identity it acts for, so a receiver that has seen no change by that key can check it (oxygenhub#129); key 3 (optional) holds the policies between the receiver's serial and the one sent with a push, ascending; key 4 (optional, only when there are any) holds signed certificates (§14.7) of short-lived keys that signed what the bundle carries or the push's attestation, their keys' and issuers' identities among the objects; a receiver keeps each that verifies with its issuer and refuses a bundle with one that does not |
767 767
| `capabilities` response | `{0: [versions], 1: repo id bytes(16), 2: [feature names]}` |
768 768
| Signed policy | `[policy bytes, [Signature]]` |
769 769
| `refs` response | `{0: [{0: name, 1: state ref, 2: [heads], 3: signed attestation or null, 4: last serial or null}], 1: repo id, 2: signed policy or null, 3: [tag], 4: [signed policy]}`; key 4 (optional) lists earlier policies ascending by serial; key 5 (optional, text or null) names the default bookmark, which a clone checks out; the attestation is present only when it describes the current state; a tag is `{0: name, 1: state ref, 2: [heads], 3: signed attestation or null}` |
…
283 unchanged lines