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)
- Bidirectional sync (permanent — restating SPEC-GIT-BRIDGE §1.3).
- SHA-256 git repositories (whole-import refusal).
- Shallow import: structurally impossible — a kit parent hash is the BLAKE3 of the translated parent, which requires the full closure. The staging mirror is always a full clone.
- Escaping schemes for git-legal/kit-illegal names or refs (refusals are reversible; a wrong escape is forever).
refs/notes,refs/replace, grafts.- Re-exporting imported history toward its origin except in passthrough/fork mode (SPEC-GIT-BRIDGE §14; see also §8).
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:
- The author Identity payload is a verbatim BYTE SLICE of the git
author line, pinned byte-exactly as: the bytes from the first byte
after the
authorkeyword+space through the last>byte that is preceded anywhere on the line by a<(interior spacing, doubled spaces, missing space before<, and stray</>inside the slice all preserved — no recomposition, so historic malformations hash identically across implementations). Lines with no such byte (no<at all, or<with no closing>anywhere after it) use the bracket-less rule: the remainder afterauthor, with one trailing match of the pattern space, decimal seconds, space,+or-, four digits stripped. A person line from which no timestamp can be parsed (no token after the identity slice, or no trailing pattern on a bracket-less line) refuses the ref — commits and tags always need a timestamp. Payloads that would be empty or exceed 4096 bytes refuse per-ref. - The committer line, both timezones, the author timestamp,
gpgsig,mergetag,encoding, and any unknown headers are not represented in the kit commit; they are recoverable from the retained raw bytes (§5). - Negative (pre-1970) committer timestamps refuse per-ref (kit timestamps are u64).
- More than 1000 parents refuses per-ref (
MAX_PARENTS). - The importer signs under
COMMIT_DOMAIN— no new signing domain. Commits carry no domain marker andverify_commithardcodes the commit domain; a separate import domain would make imported commits unverifiable everywhere. Distinguishability comes from the git-import/v1 predicate, the DSSE keyid, and the dedicated import key (§4) — the same principle SPEC-GIT-BRIDGE §11 pins for export.
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
- Implementations MUST default to a dedicated import key (offering to generate one), distinct from the user's personal commit key, and MUST print which key an import will sign with. Rationale: bounds the non-repudiation blast radius (the key signs attacker-influenceable upstream content), and makes "imported-by" legible by keyid.
- The importing key's public key is pinned in the per-remote state at first import. Import and pull MUST refuse under a different available key, naming the pinned key and the designated-importer model (§6). Key rotation means a fresh import under a new remote name (new hashes — a new fork), never a silent re-derive.
- Collaborative tracking of one upstream REQUIRES sharing the import key (an org/bot key): teammates otherwise produce unrelated forks. Cross-machine discovery of "someone already imported this" is impossible by architecture (state and attestations do not travel over kit transport); the in-store probe (§6.1) covers the same-store case, and this requirement covers the rest.
5. Provenance
Three layers, from authoritative to advisory:
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).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 thecontent_digestinput; 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.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 treatcontent_digestas 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):
dest(export-direction dirs)/source: the canonical remote identity (§8), immutable after first use. One state dir = one upstream. Fork-direction dirs recorddestinformationally only (last push target) — fork pushes lease against a fresh observation per SPEC-GIT-BRIDGE §14.1, so they are not destination-bound.direction:import,export, orfork(passthrough-enabled). A state dir MUST NOT serve import and plain export simultaneously; mixed use is refused at open. (forkcombines an import source with passthrough export per SPEC-GIT-BRIDGE §14.)signer: the pinned importer pubkey (§4).import-spec-version(§1.2).- The staging mirror (
repo.git): for import it is durable state, not a disposable cache — it is the byte-perfect original archive backing §5.2 and the passthrough object source. Deleting it forces a re-clone but loses nothing else. - The sha1↔blake3 map: rebuildable cache under the pinned key, exact same format and torn-tail semantics as export (SPEC-GIT-BRIDGE §12.3).
6.1 In-store divergence probe
Before translating, an import MUST run two checks:
- 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.
- 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
fetchsemantics:git fetchin the staging mirror, translate the new closure under the pinned key (incremental: map hits skip subgraphs), advancerefs/remotes/<remote>/*. Tracking refs are not user work: an upstream force-push moves them with a loud warning naming the rewound ref and old/new tips; map entries for abandoned commits stay valid forever (determinism).pull= fetch + fast-forward of the current branch from its tracking ref, reusing the native FF machinery and guards. Divergence refuses with the executable hint (kit merge <remote>/<branch>/kit rebase); integration is native — imported commits are ordinary kit objects.- Local work on top of imported history merges/rebases natively; replays preserve original authorship (the native replay rule).
8. Canonical remote identity
All bindings, guards, and attestation remoteUrl fields use one
normalization, remote_identity(dest):
- scp-style
[user@]host:pathrewrites tossh://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; - scheme and host lowercase; userinfo dropped; default ports
stripped per scheme (
ssh22,https443,http80,git9418); - exactly one trailing
/and one trailing.gitstripped from the path; - 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.
- Plain commit (author == committer, UTC) — the baseline.
- Committer ≠ author with non-UTC zones — committer-timestamp rule + provenance recovery.
- gpgsig commit (multi-line continuation header) — tolerant parse, signature in retained bytes only.
- latin-1 message with
encodingheader — verbatim bytes. - Historic mode
100664tree — normalize-declared-lossy. - Octopus merge (≥3 parents) — order preserved.
- Annotated tag (tagger, message) — TAG_DOMAIN importer signature.
- Signed git tag — GPG block in retained bytes; kit tag verifies under the importer key.
- > 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.