| spec | SPEC-GIT-IMPORT |
|---|---|
| version | 1 |
| status | draft-normative |
| audience | implementers of the git→mkit import bridge and its verifiers |
Status: Normative for the git→mkit import direction. Scope: the mapping from git objects to mkit 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 mkit objects losslessly through carrier headers; import is a signed translation across a trust boundary: every imported commit and tag is a new mkit object signed by the importer, attributing the original author and binding the original git bytes as provenance. The result is a downstream fork — its mkit hashes are a function of the importer's signing key, and two importers of the same upstream produce unrelated mkit histories by design (§6).
git blob → mkit blob (≤ 1 MiB) or chunk blobs +
manifest (> 1 MiB, pinned FastCDC)
git tree → mkit tree (re-sorted; names validated)
git commit → mkit commit, SIGNED BY THE IMPORTER
git annotated/signed tag → mkit 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 an mkit
signature.
The mapping is defined for mkit 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). mkit signing is deterministic (RFC 8032), so re-running an import reproduces byte-identical mkit objects, the sha1↔blake3 map is a rebuildable cache under the same key, and a crashed import is resumed by re-running it.
- Bidirectional sync (permanent — restating SPEC-GIT-BRIDGE §1.3).
- SHA-256 git repositories (whole-import refusal).
- Shallow import: structurally impossible — an mkit 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/mkit-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).
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.
git blob bytes map verbatim. Content at or below the 1 MiB chunking
threshold becomes one mkit 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.
mkit 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 mkit commit; they are recoverable from the retained raw bytes (§5). - Negative (pre-1970) committer timestamps refuse per-ref (mkit 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.
Entries re-sort from git order (directories keyed name + "/") to
mkit 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 mkit 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
mkit 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 (mkit'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/
.mkit case-insensitive, end in dot/space, are Windows device stems
(aux.c-class), exceed 255 bytes, or contain backslash refuse the
ref.
Annotated and signed git tags map to mkit 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 mkit 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 mkit 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 mkit tag object has no annotation slots, so tag
provenance lives only in §5's retained bytes + attestation.
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.
- 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 mkit transport); the in-store probe (§6.1) covers the same-store case, and this requirement covers the rest.
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://github.com/officialunofficial/mkit/spec/predicate/git-import/v1.subject[0]={name: <full mkit ref>, digest: {blake3: <mkit head hash>}}. Predicate (JCS key order):{ "gitCommit": "<40hex sha1 of the upstream head>", "refName": "<full mkit 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 (
.mkit/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 mkit 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 mkit-side signed attestation, not the sha1-addressed storage. The head attestation (this section's opening paragraphs) has a BLAKE3 subject digest over the mkit 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.
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. mkit-git-bridge's current dependency (sha1 = "0.11") is a plain implementation and does not meet this requirement.
State lives in .mkit/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).
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 ; pull from the designated importer over mkit 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.
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 (mkit merge <remote>/<branch>/mkit rebase); integration is native — imported commits are ordinary mkit objects.- Local work on top of imported history merges/rebases natively; replays preserve original authorship (the native replay rule).
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.
Pinned under rust/tests/golden/git-import/ with the standard
MANIFEST convention; each vector records the source git object bytes,
the imported mkit 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; mkit 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.
| 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. |
| Invariant | Enforced by |
|---|---|
| Re-running an import reproduces byte-identical mkit 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 mkit-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.