KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

SPEC-GIT-IMPORT — importer-signed git→kit translation (v1)

Status: Normative for the git→kit import direction. Scope: the mapping from git objects to kit v1 objects under the importer-signed policy, the refusal matrix, provenance, per-remote state and key binding, canonical remote identity, and pull/divergence semantics. Shared byte-level encodings (hex/base64 forms, ref grammars, chunking parameters) are defined by SPEC-GIT-BRIDGE, SPEC-OBJECTS, SPEC-SIGNING, SPEC-REFS, and SPEC-FASTCDC; this spec only adds the inbound rules.

Import is not the inverse of export. Export round-trips kit objects losslessly through carrier headers; import is a signed translation across a trust boundary: every imported commit and tag is a new kit object signed by the importer, attributing the original author and binding the original git bytes as provenance. The result is a downstream fork — its kit hashes are a function of the importer's signing key, and two importers of the same upstream produce unrelated kit histories by design (§6).


1. Model

1.1 Direction and trust

git blob                  → kit blob (≤ 1 MiB) or chunk blobs +
                            manifest (> 1 MiB, pinned FastCDC)
git tree                  → kit tree (re-sorted; names validated)
git commit                → kit commit, SIGNED BY THE IMPORTER
git annotated/signed tag  → kit tag, SIGNED BY THE IMPORTER
git lightweight tag       → bare refs/tags/<name> ref
git gitlink (160000)      → REFUSED (per-ref)

The importer's Ed25519 signature asserts exactly: "I vouch that these bytes are my translator's faithful output for the named upstream object." It is never an authorship claim — original authorship rides in the author/tagger Identity (§3.2), and SPEC-SIGNING §6 already separates attribution from the verification key. A git GPG/SSH signature on the source commit/tag is carried in the retained raw bytes (§5) and is not representable as a kit signature.

1.2 Version keying and determinism

The mapping is defined for kit object schema 0x01 and is pinned by an import-spec version recorded in the per-remote state (§6). Implementations MUST refuse to incrementally extend an import whose recorded import-spec version they do not implement (forcing an explicit re-import) — translation-rule drift between versions would silently fork hashes for the same key.

Import is a pure function of (upstream git bytes, importer signing key, import-spec version). kit signing is deterministic (RFC 8032), so re-running an import reproduces byte-identical kit objects, the sha1↔blake3 map is a rebuildable cache under the same key, and a crashed import is resumed by re-running it.

1.3 Non-goals (v1)


2. Reading the upstream

Implementations SHOULD obtain upstream objects via a full git clone --mirror into the per-remote staging repository (§6) and read objects with git cat-file --batch (git owns wire protocol, auth, and pack storage; the bridge owns only translation). Commit order MUST be a topological parents-first order (git rev-list --reverse --topo-order). The inbound parser MUST be tolerant of everything git accepts — multi-line continuation headers (gpgsig, mergetag), unknown headers, the encoding header, malformed person lines in historic commits — because this is an untrusted-input boundary; tolerance means parse and carry or refuse loudly, never crash or silently alter.

objectFormat = sha256 upstreams MUST be refused at clone time.


3. Inbound object mapping

3.1 Blobs

git blob bytes map verbatim. Content at or below the 1 MiB chunking threshold becomes one kit blob; above it, the pinned FastCDC chunker (SPEC-FASTCDC) produces chunk blobs + a chunk_size = 0 manifest — exactly the writer-side rule, so imported stores are exportable (SPEC-GIT-BRIDGE §4 refusals never trigger on them). SPEC-FASTCDC leaves the threshold an implementer choice; for importers CHUNK_THRESHOLD = 1 MiB is normative (a different threshold would fork hashes across implementations). Blobs over worktree::MAX_FILE_BYTES (1 GiB, the per-file cap) refuse per-ref.

3.2 Commits

kit field        ← git source
tree_hash         ← translated tree
parents           ← translated parents, order preserved
author            ← Identity::Opaque(author name + " <" + email + ">")
signer            ← the importer key (§4)
message           ← message bytes VERBATIM (no UTF-8 constraint;
                    `encoding` header tolerated and dropped)
timestamp         ← git COMMITTER epoch seconds
message_hash      ← zero
content_digest    ← BLAKE3(framed raw git commit bytes) (§5; advisory)
signature         ← Ed25519 by the importer under COMMIT_DOMAIN

Pinned rules:

3.3 Trees

Entries re-sort from git order (directories keyed name + "/") to kit byte-lex order. Canonical modes map 100644→0x01, 40000→0x02, 120000→0x03, 100755→0x04. Historic non-canonical mode SPELLINGS normalize to their canonical equivalent's kit mode, enumerated exhaustively: 100664→0x01, 100640→0x01, 100600→0x01, 040000→0x02 (zero-padded directory). Any other mode string refuses per-ref. Normalization is declared-lossy (warned once per ref) — except in a state dir whose recorded direction is fork (§6), where a normalized tree could not reproduce its original sha1 and the affected ref MUST refuse instead. 160000 (gitlink/submodule) refuses per-ref with an actionable message. Two structural refusals close gaps git can represent but kit cannot decode: entry names that collide byte-equal after the re-sort (a file and a directory of one name) refuse per-ref, and trees nesting beyond 128 levels (kit's MAX_TREE_DEPTH defense) refuse per-ref. Entry names are validated by SPEC-OBJECTS §4.1 (deserialize-time rules — non-negotiable): names that are .git/ .kit case-insensitive, end in dot/space, are Windows device stems (aux.c-class), exceed 255 bytes, or contain backslash refuse the ref.

3.4 Tags

Annotated and signed git tags map to kit tag objects signed by the importer under TAG_DOMAIN (same vouch semantics as commits; the original tagger line maps to the tagger Identity by the §3.2 rule and the kit tag timestamp is the tagger epoch, negative-refused like commits; git tag GPG signatures ride in the retained raw bytes only). Historic tagger-less tags (git v0.99 era) take the PINNED sentinel Identity::Opaque("(no tagger)") and timestamp 0 — any other choice would fork tag hashes across implementations. The tag name must satisfy the kit single-segment tag grammar (SPEC-GIT-BRIDGE §7.1) or the tag refuses. Lightweight tags map to bare refs. Tag targets follow the object mapping; tag→tag chains are bounded at 16. The kit tag object has no annotation slots, so tag provenance lives only in §5's retained bytes + attestation.

3.5 Refs

Upstream refs/heads/* and refs/tags/* map to refs/remotes/<remote>/<branch> tracking refs and refs/tags/<name> respectively (fresh-clone form also sets refs/heads/<default> and HEAD). Ref names outside the SPEC-REFS §3 grammar (@, +, unicode, .lock suffix, HEAD final segment) refuse per-ref. The refusal UX matches export: skip-and-warn per ref, non-zero exit when everything was skipped.


4. The importer key


5. Provenance

Three layers, from authoritative to advisory:

  1. git-import/v1 attestation (authoritative): one DSSE/in-toto attestation per imported ref head per fetch, signed with the importer's configured signer. predicateType = https://kit.storage/spec/predicate/git-import/v1. subject[0] = {name: <full kit ref>, digest: {blake3: <kit head hash>}}. Predicate (JCS key order):

    {
      "gitCommit": "<40hex sha1 of the upstream head>",
      "refName": "<full kit ref name>",
      "remoteUrl": "<canonical remote identity, §8>",
      "schemaVersion": 1,
      "specVersion": 1
    }
    

    Because parents are inside signed commit bytes, a head attestation transitively pins the imported closure. Envelopes are stored like any attestation (.kit/attestations/<commit>/…). Importers SHOULD skip re-minting for a head whose recorded imported state is unchanged (mirroring SPEC-GIT-BRIDGE §11's guidance).

  2. Retained raw bytes: the original git commit and tag object bytes in their FRAMED form — "<type> <len>\0" + body — stored under the per-remote state dir, keyed by their sha1 preimage purely as a lookup key (this framing is normative for both retention and the content_digest input; body-only would fork digests across implementations). Small (commits/tags only — trees/blobs are recoverable from the staging mirror and re-derivable from the kit twins). Consistent with SPEC-GIT-BRIDGE §2/§11: sha1 here is a locator, never a security proof. Keying storage by sha1 does not make the retained bytes trustworthy on its own — an adversary who can compute a sha1 collision (practical for chosen-prefix collisions since 2020) could in principle plant different bytes under the same key. What actually establishes trust is described in point 3 below.

  3. The proof surface is the kit-side signed attestation, not the sha1-addressed storage. The head attestation (this section's opening paragraphs) has a BLAKE3 subject digest over the kit commit — the same "locator vs. subject" split SPEC-GIT-BRIDGE §11 uses for the export direction — and an Ed25519 signature over that subject. That signature is the actual proof surface: it transitively pins the imported closure because parents are inside the signed commit bytes (this section's opening paragraph). content_digest = BLAKE3(raw git commit bytes) is carried as an advisory field, excluded from signing bytes (SPEC-OBJECTS §5.1) and therefore malleable in isolation — a signature-preserving sibling with a forged slot is constructible. Verifiers MUST treat content_digest as a hint for locating/diffing retained bytes, never as something the signature vouches for.

Correspondence audit: a signed head binds the kit closure but does not by itself prove that a mutable mapping cache pairs each Git object with the correct kit object. An auditor MUST derive each reachable imported object's unsigned fields from retained Git bytes under the recorded import-spec version, compare all translated fields and edges, and verify its signature with the pinned public key. It MUST validate a required head's attestation claim against the exact subject hash, full ref, Git ID, canonical source, predicate and schema versions. At least one complete matching claim is required; unrelated extra envelopes do not qualify and may be ignored. Native bridge objects' mapped parent/tree edges are checked against the same correspondence graph. Audit requires no private signing material and MUST NOT rewrite objects or caches. See SPEC-GIT-BRIDGE §14.3 for traversal limits and failure behavior.

Collision-resistant hashing (normative requirement): because sha1 here still functions as a lookup key over data an adversary may control (untrusted upstream Git history), every sha1 computed over upstream bytes in the bridge/import path MUST use a collision-detecting SHA-1 construction (rejecting known cryptanalytic collision patterns), not a plain SHA-1 implementation — matching real Git's own practice for exactly this reason. kit-git-bridge's current dependency (sha1 = "0.11") is a plain implementation and does not meet this requirement.


6. Per-remote state and guardrails

State lives in .kit/git/<remote>/ (layout implementation-defined, non-normative except for the binding semantics below):

6.1 In-store divergence probe

Before translating, an import MUST run two checks:

  1. State-dir identity check (structural): if any state dir in this repository records the same canonical remote identity (§8), refuse unless it is the one being used — this catches re-imports regardless of how far upstream has advanced.
  2. Content probe (best-effort): walking back from each upstream head through first parents (bounded; until the first hit or the walk ends), if a commit whose content_digest == BLAKE3(candidate's raw bytes) exists in the store but was signed by a different key, refuse with guidance ("this upstream is already imported here under key <hex8>; pull from the designated importer over kit transport, or install that key"). A linear store scan is an acceptable lookup mechanism (imports are rare, interactive operations); an index is permitted but not required.

The probe is best-effort (content_digest is advisory) — it catches the realistic accident, not an adversary.


7. Pull, divergence, and rewrites


8. Canonical remote identity

All bindings, guards, and attestation remoteUrl fields use one normalization, remote_identity(dest):

  1. scp-style [user@]host:path rewrites to ssh://host/path (honesty note: a home-relative scp path and an absolute ssh:// path can be different repos on a generic server; the conflation only widens the guard — fail-closed). IPv6 literals keep their brackets: [::1]:path → ssh://[::1]/path;
  2. scheme and host lowercase; userinfo dropped; default ports stripped per scheme (ssh 22, https 443, http 80, git 9418);
  3. exactly one trailing / and one trailing .git stripped from the path;
  4. local paths AND file:// URLs (file:///p, file://localhost/p) collapse to the symlink-resolved absolute local path.

Equivalence examples (all one identity): git@github.com:org/repo.git ≡ ssh://github.com/org/repo ≡ ssh://GIT@GITHUB.COM:22/org/repo/. The origin guard (SPEC-GIT-BRIDGE §14.2) compares canonical identities: plain (non-passthrough) export MUST refuse a destination whose identity matches any recorded import source. This is a safety net against accidents, not a security boundary — mirrors and redirects are undetectable; the push lease remains the backstop.


9. Test vectors (implementer MUST produce)

Pinned under rust/tests/golden/git-import/ with the standard MANIFEST convention; each vector records the source git object bytes, the imported kit object bytes under the fixed test key, and both ids. Single exception: vector 9's > 1 MiB content is regenerated deterministically by the golden test rather than committed; its two ids are pinned like every other vector (same carve-out as SPEC-GIT-BRIDGE §13's chunked vector). Refusal vectors (gitlink tree, reserved tree-entry name, negative timestamp, out-of-grammar tag name) pin their TYPED refusal in REFUSALS.txt alongside the success vectors.

  1. Plain commit (author == committer, UTC) — the baseline.
  2. Committer ≠ author with non-UTC zones — committer-timestamp rule + provenance recovery.
  3. gpgsig commit (multi-line continuation header) — tolerant parse, signature in retained bytes only.
  4. latin-1 message with encoding header — verbatim bytes.
  5. Historic mode 100664 tree — normalize-declared-lossy.
  6. Octopus merge (≥3 parents) — order preserved.
  7. Annotated tag (tagger, message) — TAG_DOMAIN importer signature.
  8. Signed git tag — GPG block in retained bytes; kit tag verifies under the importer key.
  9. > 1 MiB blob — FastCDC manifest equals the writer-side rule.

Refusal vectors (assert the typed refusal, not bytes): gitlink tree, aux.c tree name, negative timestamp, v1.0+build ref name, sha256 repo marker.

Every vector MUST be byte-stable across two runs (determinism) and MUST verify_commit/verify_tag under the test key.


10. Version history

Version Changes
1 Initial importer-signed mapping: blobs/trees/commits/tags, refusal matrix, dedicated-key + pinning model, three-layer provenance, canonical remote identity, fetch/pull semantics, golden vectors.

11. Invariants

Invariant Enforced by
Re-running an import reproduces byte-identical kit objects import is a pure function of (upstream bytes, importer key, import-spec version); RFC 8032 signing is deterministic (§1.2)
Two importers under the same key produce identical hashes; under different keys, unrelated forks — never a silent mix hashes are a function of the signing key (§1.2); the pinned key refuses imports under any other key (§4)
Translation-rule drift can never silently fork hashes for one key recorded import-spec version; unimplemented versions refuse incremental extension (§1.2)
Every imported commit/tag carries a verifiable importer vouch, never an authorship claim importer Ed25519 under the existing COMMIT_DOMAIN/TAG_DOMAIN; attribution rides in the author/tagger Identity (§1.1, §3.2, §3.4)
Historic malformations (person lines, modes, tagger-less tags) hash identically across implementations byte-exact identity-slice rule, exhaustive mode-spelling table, pinned (no tagger) sentinel (§3.2, §3.3, §3.4)
Untrusted upstream input is never silently altered parse-and-carry or typed per-ref refusal — the §3 refusal matrix; never crash or normalize undeclared (§2, §3)
Imported stores are always exportable normative 1 MiB threshold + writer-side FastCDC rule, so SPEC-GIT-BRIDGE §4 refusals never trigger (§3.1)
The translation is byte-auditable after the fact framed raw commit/tag bytes retained sha1-addressed; head attestation transitively pins the signed closure (§5.1, §5.2)
A forged content_digest cannot defeat verification the slot is advisory and excluded from signing bytes; the kit-side signed attestation (BLAKE3 subject + Ed25519 signature), not the sha1-addressed retained bytes, is the proof surface (§5.3)
One state dir tracks exactly one upstream, in exactly one direction canonical remote identity (§8) immutable after first use; mixed import/plain-export use refused at open (§6)
Re-importing an already-imported upstream in the same store is caught state-dir identity check (structural) + content probe (best-effort) (§6.1)
An upstream force-push never silently rewrites local work tracking refs move with a loud warning; the current branch advances only by fast-forward (§7)

The content probe (§6.1) is explicitly best-effort — it catches the realistic accident, not an adversary; the pinned-key refusal (§4) is the structural guarantee.