Last Modified: May 29, 2025
This document describes gittuf, a security layer for Git repositories. With gittuf, any developer who can pull from a Git repository can independently verify that the repository's security policies were followed. gittuf's policy, inspired by The Update Framework (TUF), handles key management for all trusted developers in a repository, allows for setting permissions for repository namespaces such as branches, tags, and files, and provides protections against attacks targeting Git metadata. At the same time, gittuf is backwards compatible with existing source control platforms ("forges") such as GitHub, GitLab, and Bitbucket. gittuf is currently an incubating project at the Open Source Security Foundation (OpenSSF) as part of the Supply Chain Integrity Working Group. The core concepts of gittuf described in this document have been peer reviewed.
This document is scoped to describing how gittuf's write access control policies are applied to Git repositories. Other additions to gittuf's featureset are described in standalone gittuf Augmentation Proposals (GAPs).
This document uses several terms or phrases in specific ways. These are defined here.
A Git reference is a "simple name" that typically points to a particular Git commit. Generally, development in Git repositories are centered in one or more refs, and they're updated as commits are added to the ref under development. By default, Git defines two of refs: branches ("heads") and tags. Git allows for the creation of other arbitrary refs that users can store other information as long as they are formatted using Git's object types.
Git employs a content addressed object store, with support for four types of objects. An essential Git object is the "commit", which is a self-contained representation of the whole repository. Each commit points to a "tree" object that represents the state of the files in the root of the repository at that commit. A tree object contains one or more entries that are either other tree objects (representing subdirectories) or "blob" objects (representing files). The final type of Git object is the "tag" object, used as a static pointer to another Git object. While a tag object can point to any other Git object, it is frequently used to point to a commit.
Repository
|
|-- refs
| |
| |-- heads
| | |-- main (refers to commit C)
| | |-- feature-x (refers to commit E)
| |
| |-- tags
| | |-- v1.0 (refers to tag v1.0)
| |
| |-- arbitrary
| |-- custom-ref (formatted as Git object type)
|
|-- objects
|-- A [Initial commit]
|-- B [Version 1.0 release]
|-- C [More changes on main]
|-- D [Initial commit on feature-x]
|-- E [More changes on feature-x]
|-- v1.0 [Tag object referring to commit B]
In a Git repository, an "actor" is any party, human or bot, who makes changes to the repository. These changes can involve any part of the repository, such as modifying files, branches or tags. In gittuf, each actor is identified by a unique signing key that they use to cryptographically sign their contributions. gittuf uses cryptographic signatures to authenticate actors as these signatures can be verified by anyone who has the corresponding public key, fundamental to gittuf's mission to enable independent verification of repository actions. Note that gittuf does not rely on Git commit metadata (e.g., author email, committer email) to identify the actor who created it, as that may be trivially spoofed.
In practice, a gittuf policy allows an actor to make certain changes by granting trust to the actor's signing key to make those changes. To maintain security, all actions made in the repository, such as adding or modifying files, are checked for authenticity. This is done by verifying the digital signature attached to the action, which must match the trusted public key associated with the actor who is supposed to have made the change.
The term "state" refers to the latest values or conditions of the tracked references (like branches and tags) in a Git repository. These are determined by the most recent entries in the reference state log. Note that when verifying changes in the repository, a workflow may only verify specific references rather than all state updates in the reference state log.
The following threat model is taken from the peer reviewed publication describing gittuf.
We consider the standard scenario where a forge is used to manage a Git repository on a centralized synchronization point. This forge can be a publicly hosted solution (e.g., the github.com service), or self-hosted on premises by an enterprise. Either option exposes the forge instance to both external attackers and insider threats. External attackers may circumvent security measures and compromise the version control system, manifesting themselves as advanced persistent threats (APT) and making unnoticed changes to the system. Similarly, insider threats may be posed by rogue employees with escalated privileges who abuse their authority to make unnoticed changes.
To protect the integrity of the repository’s contents, the maintainers of the repository define security controls such as which contributors can write to different parts of the repository. gittuf is meant to protect against scenarios where any party, individual developers, bots that make changes, or the forge itself, may be compromised and act in an arbitrarily malicious way as seen in prior incidents. This includes scenarios such as:
- T1: Modifying configured repository security policies, such as to weaken them
- T2: Tampering with the contents of the repository’s activity log, such as by reordering, dropping, or otherwise manipulating log entries
- T3: Subverting the enforcement of security policies, such as by accepting invalid changes instead of rejecting them
Note that we consider out of scope a freeze attack, where the forge serves stale data, as development workflows involve a substantial amount of out-of-band communication which prevents such attacks from going unnoticed. We similarly consider weaknesses in cryptographic algorithms as out of scope.
gittuf records additional metadata describing the repository's policy and
activity in the repository itself. Effectively, gittuf treats security policies,
activity information, and policy decisions as a content tracking problem. To
avoid collisions with regular repository contents, gittuf stores its metadata in
custom references under refs/gittuf/.
Note: This section assumes some prior knowledge of the TUF specification.
The repository's policy metadata handles the distribution of the repository's
trusted keys (representing actors) as well as write access control rules. There
are two types of metadata used by gittuf, which are stored in a custom reference
refs/gittuf/policy.
gittuf's policy metadata includes root of trust metadata, which establishes why the policy must be trusted. The root of trust metadata (similar to TUF's root metadata) declares the keys belonging to the repository owners as well as a numerical threshold that indicates the minimum number of signatures for the metadata to be considered valid. The root of trust metadata is signed by a threshold of root keys, and the initial set of root keys for a repository must be distributed using out-of-band mechanisms or rely on trust-on-first-use (TOFU). Subsequent changes to the set of root keys are handled in-band, with a new version of the root of trust metadata created. This new version must be signed by a threshold of root keys trusted in the previous version.
The rules protecting the repository's namespaces are declared in one or more rule files. A rule file is similar to TUF's targets metadata. It declares the public keys for the trusted actors, as well as namespaced "delegations" which specify protected namespaces within the repository and which actors are trusted to write to them.
A threshold of trusted actors for any delegation (or rule) can extend this trust to other actors by signing a new rule file with the same name as the delegation. In this rule file, they can add the actors who must be trusted for the same (or a subset) of namespaces.
All repositories must contain a primary rule file (typically called "targets.json" to match TUF's behavior). This rule file may contain no rules, signifying that no repository namespaces are protected. The primary rule file derives its trust directly from the root of trust metadata; it must be signed by a threshold of actors trusted to manage the repository's primary rule file. All other rule files derive their trust directly or indirectly from the primary rule file through delegations.
In this example, the repository administrator grants write permissions to Carol
for the main branch, to Alice for the alice-dev branch, and to Bob for the
/tests folder (under any of the existing branches).
A significant difference between typical TUF metadata and those used by gittuf
is in the expectations of the policies. Typical TUF deployments are explicit
about the artifacts they are distributing. Any artifact not listed in TUF
metadata is rejected. In gittuf, policies are written only to express
restrictions. As such, when verifying changes to unprotected namespaces,
gittuf must allow any key to sign for these changes. This means that after all
explicit policies (expressed as delegations) are processed, and none apply to
the namespace being verified, an implicit allow-rule is applied, allowing
verification to succeed.
The following example is taken from the peer reviewed publication of gittuf's design. It shows a gittuf policy state with its root of trust and three distinct rule files connected using delegations. The root of trust declares the trusted signers for the next version of the root of trust as well as the primary rule file. Signatures are omitted.
rootOfTrust:
keys: {R1, R2, R3, P1, P2, P3}
signers:
rootOfTrust: (2, {R1, R2, R3})
primary: (2, {P1, P2, P3})
ruleFile: primary
keys: {Alice, Bob, Carol, Helen, Ilda}
rules:
protect-main-prod: {git:refs/heads/main,
git:refs/heads/prod}
-> (2, {Alice, Bob, Carol})
protect-ios-app: {file:ios/*}
-> (1, {Alice})
protect-android-app: {file:android/*}
-> (1, {Bob})
protect-core-libraries: {file:src/*}
-> (2, {Carol, Helen, Ilda})
ruleFile: protect-ios-app
keys: {Dana, George}
rules:
authorize-ios-team: {file:ios/*}
-> (1, {Dana, George})
ruleFile: protect-android-app
keys: {Eric, Frank}
rules:
authorize-android-team: {file:android/*}
-> (1, {Eric, Frank})
gittuf leverages a "Reference State Log (RSL)" to track changes to the repository's references. In addition, gittuf uses the in-toto Attestation Framework to record other repository activity such as code review approvals.
Note: This document presents a summary of the RSL. For a full understanding of the attacks mitigated by the RSL, please refer to the academic papers underpinning gittuf's design.
The Reference State Log contains a series of entries that each describe some change to a Git ref. Such entries are known as RSL reference entries. Each entry contains the ref being updated, the new location it points to, and a hash of the parent RSL entry. The entry is signed by the actor making the change to the ref.
Additionally, the RSL supports annotation entries that refer to prior reference entries. An annotation entry can be used to attach additional user-readable messages to prior RSL entries or to mark those entries as revoked.
Given that each entry points to its parent entry using its hash, an RSL is a hash chain. gittuf's implementation of the RSL uses Git's underlying Merkle graph. Generally, gittuf is designed to ensure the RSL is linear but a privileged attacker may be able to cause the RSL to branch, resulting in a fork* attack where different actors are presented different versions of the RSL. The feasibility and implications of such an attack are discussed later in this document.
The RSL is tracked at refs/gittuf/reference-state-log, and is implemented as a
distinct commit graph. Each commit corresponds to one entry in the RSL, and
standard Git signing mechanisms are employed for the actor's signature on the
RSL entry. The latest entry is identified using the tip of the RSL Git ref.
Note that the RSL and liveness of the repository in Git remove the need for some traditional TUF roles. As the RSL records changes to other Git refs in the repository, it incorporates TUF's snapshot role properties. At present, gittuf does not include an equivalent to TUF's timestamp role to guarantee the freshness of the RSL. This is because the timestamp role in the context of gittuf at most provides a non-repudiation guarantee for each claim of the RSL's tip. The use of an online timestamp does not guarantee that actors will receive the correct RSL tip. This may evolve in future versions of the gittuf design.
These entries are the standard variety described above. They contain the name of the reference they apply to and a target ID. As such, they have the following structure.
RSL Reference Entry
ref: <ref name>
targetID: <target ID>
number: <number>
The targetID is typically the ID of a commit for references that are branches.
However, for entries that record the state of a Git tag, targetID is the ID of
the annotated tag object.
Apart from regular entries, the RSL can include annotations that apply to prior RSL entries. Annotations can be used to add more information as a message about a prior entry, or to explicitly mark one or more entries as ones to be skipped. This semantic is necessary when accidental or possibly malicious RSL entries are recorded. Since the RSL history cannot be overwritten, an annotation entry must be used to communicate to gittuf clients to skip the corresponding entries. Annotations have the following schema.
RSL Annotation Entry
entryID: <RSL entry ID 1>
entryID: <RSL entry ID 2>
...
skip: <true/false>
number: <number>
-----BEGIN MESSAGE-----
<message>
------END MESSAGE------
Here's a sample RSL, with the output taken from gittuf rsl log:
entry a5ea2c6ee7e8b577f6be6fcee5b06e6cac7166fa (skipped)
Ref: refs/heads/main
Target: 6cb8e5c546eab3d0e1d245014de8003febb8e9b3
Number: 5
Annotation ID: cccfb6f27b2a71c33e9a55bc82f084e2445aa398
Skip: yes
Number: 6
Message:
Skipping RSL entry
entry 40c82851f78c7018f4c360030a83923b0925c28d
Ref: refs/gittuf/policy
Target: b7cf91ec9b5b6b17334ab1378dc85375236524f5
Number: 4
entry 94c153bff6d684a956ed27f0abd70624e875657c
Ref: refs/gittuf/policy-staging
Target: b7cf91ec9b5b6b17334ab1378dc85375236524f5
Number: 3
entry fed977a5ca07e566af3a37808284dc7c5a67d6dc
Ref: refs/gittuf/policy-staging
Target: dcbb536bd86a69e555692aec7b65c20de8257ee2
Number: 2
entry e026a62f1c63c6db58bb357f9a85cafe05c64fb6
Ref: refs/gittuf/policy-staging
Target: 603fc733218a0a1e54ccde47d1d9864f67e0bb75
Number: 1
Specifically, the latest reference entry
a5ea2c6ee7e8b577f6be6fcee5b06e6cac7166fa has been skipped by an annotation
entry cccfb6f27b2a71c33e9a55bc82f084e2445aa398.
The commit object for the reference entry is as follows:
~/tmp/repo $ git cat-file -p a5ea2c6ee7e8b577f6be6fcee5b06e6cac7166fa
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 40c82851f78c7018f4c360030a83923b0925c28d
author Aditya Sirish A Yelgundhalli <ayelgundhall@bloomberg.net> 1729514863 -0400
committer Aditya Sirish A Yelgundhalli <ayelgundhall@bloomberg.net> 1729514863 -0400
gpgsig -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg8g2CmHSb7guzi6MUNgwHUQnxPN
X1x8urScZyJrUB6MMAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
AAAAQGQMSviwqF+cE/wgEo0U73vu86YHi4f5crzzFIctjyMGOOy2isYfHgGvSzs5bv6V2Q
EtMumBSVbCxvnRqJpiFAs=
-----END SSH SIGNATURE-----
RSL Reference Entry
ref: refs/heads/main
targetID: 6cb8e5c546eab3d0e1d245014de8003febb8e9b3
number: 5Similarly, the commit object for the annotation entry is as follows:
~/tmp/repo $ git cat-file -p cccfb6f27b2a71c33e9a55bc82f084e2445aa398
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent a5ea2c6ee7e8b577f6be6fcee5b06e6cac7166fa
author Aditya Sirish A Yelgundhalli <ayelgundhall@bloomberg.net> 1729514924 -0400
committer Aditya Sirish A Yelgundhalli <ayelgundhall@bloomberg.net> 1729514924 -0400
gpgsig -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg8g2CmHSb7guzi6MUNgwHUQnxPN
X1x8urScZyJrUB6MMAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
AAAAQNf32yJvhGfLIIeeStHgkSB7iuRGJl6LhbRTpX/q49lUu4TrEiCeGa3H8LMJ/5D1EE
in3QAhlzdowYnmCKglTAw=
-----END SSH SIGNATURE-----
RSL Annotation Entry
entryID: a5ea2c6ee7e8b577f6be6fcee5b06e6cac7166fa
skip: true
number: 6
-----BEGIN MESSAGE-----
U2tpcHBpbmcgUlNMIGVudHJ5
-----END MESSAGE-----gittuf makes use of the signing capability provided by Git for commits and tags
significantly. However, it is sometimes necessary to attach more than a single
signature to a Git object or repository action. For example, a policy may
require more than one developer to sign off and approve a change such as merging
something to the main branch. To support these workflows (while also remaining
compatible with standard Git clients), gittuf uses the concept of "detached
authorizations", implemented using signed in-toto
attestations. Attestations are tracked
in the custom Git reference refs/gittuf/attestations. The gittuf design
currently supports the "reference authorization" type to represent code review
approvals. Other types may be added to this document or via GAPs
in future.
A reference authorization is an attestation that accompanies an RSL reference entry, allowing additional developers to issue signatures authorizing the change to the Git reference in question. Its structure is similar to that of a reference entry:
TargetRef string
FromTargetID string
ToTargetID string
The TargetRef is the Git reference the authorization is for, while
FromTargetID and ToTargetID record the change in the state of the reference
authorized by the attestation (as Git hashes). The information pertaining to the
prior state of the Git reference is explicitly recorded in the attestation
unlike a standard RSL reference entry. This is because, for a reference entry,
this information can be implicitly identified using the RSL by examining the
previous entry for the reference in question. If the authorization is for a
brand new reference (say a new branch or any tag), FromTargetID must be set to
zero. For a change to a branch, ToTargetID pre-computes the Git merge tree
resulting from the change being approved. Thus, when verifying the change to the
branch, it must be followed by an RSL reference entry that points to a commit
which has the same Git tree ID. For a tag, ToTargetID records the Git object
the tag object is expected to point to.
Reference authorizations are stored in a directory called
reference-authorizations in the attestations namespace. Each authorization
must have the in-toto predicate type:
https://gittuf.dev/reference-authorization/v<VERSION>.
gittuf introduces some new workflows that are gittuf-specific, such as the creation of policies and their verification. In addition, gittuf interposes in some Git workflows so as to capture repository activity information.
When the policy is initialized or updated (this can be a change to the root of
trust metadata or one or more rule files), a new policy state is created that
contains the full set of gittuf policy metadata. This is recorded as a commit in
the custom ref used to track the policy metadata (typically
refs/gittuf/policy). In turn, the commit to the custom ref is recorded in the
RSL, indicating the policy state to use for subsequent changes in the
repository.
As the RSL must be linear with no branches, gittuf employs a variation of the
Secure_Fetch and Secure_Push workflows described in the
RSL academic paper.
Note that gittuf can be used even if the synchronization point is not
gittuf-enabled. The repository can host the gittuf namespaces which other gittuf
clients can pull from for verification. In this example, a gittuf client with a
changeset to commit to the dev branch (step 1), creates in its local repository
a new commit object and the associated RSL entry (step 2). These changes are
pushed next to a remote Git repository (step 3), from where other gittuf or
legacy Git clients pull the changes (step 4).
Before local RSL changes can be made or pushed, it is necessary to verify that they are compatible with the remote RSL state. If the remote RSL has entries that are unavailable locally, entries made locally will be rejected by the remote. For example, let the local RSL tip be entry A and the new entry be entry C. If the remote has entry B after A with B being the tip, attempting to push C which also comes right after A will fail. Instead, the local RSL must first fetch entry B and then create entry C. This is because entries in the RSL must be made serially. As each entry includes the ID of the previous entry, a local entry that does not incorporate the latest RSL entry on the remote is invalid. The workflow is as follows:
- Fetch remote RSL to the local remote tracker
refs/remotes/origin/gittuf/reference-state-log. - If the last entry in the remote RSL is the same as the local RSL, terminate successfully.
- Perform the verification workflow for the new entries in the remote RSL,
incorporating remote changes to the local policy namespace. The verification
workflow is performed for each Git reference in the new entries, relative to
the local state of each reference. If verification fails, abort and warn
user. Note that the verification workflow must fetch each Git reference to
its corresponding remote tracker,
refs/remotes/origin/<ref>. - For each modified Git reference, update the local state. As all the refs have
been successfully verified, each ref's remote state can be applied to the
local repository, so
refs/heads/<ref>matchesrefs/remotes/origin/<ref>. - Set local RSL to the remote RSL's tip.
NOTE: Some aspects of this workflow are under discussion and are subject to change. The gittuf implementation does not implement precisely this workflow. Specifically, the implementation does not verify new entries in the remote automatically. Additionally, the RSL may contain entries for references a client does not have, making verification of those entries unfeasible. See gittuf#708.
- Execute
RSLFetchrepeatedly until there are no new RSL entries in the remote RSL. Every time there is a remote update, the user must be prompted to fetch and re-apply their changes to the RSL. This process could be automated but user intervention may be needed to resolve conflicts in the refs they modified. Changes to the gittuf policy must be fetched and applied locally. - Verify the validity of the RSL entries being submitted using locally available gittuf policies to ensure the user is authorized for the changes. If verification fails, abort and warn user.
- Perform an atomic Git push to the remote of the RSL as well as the modified
Git references. If the push fails, it is likely because another actor pushed
their changes first. Restart the
RSLPushworkflow.
NOTE: Some aspects of this workflow are under discussion and are subject to change. The gittuf implementation does not implement precisely this workflow. This workflow is closely related to other push operations performed in the repository, and therefore, this section may be incorporated with other workflows. See gittuf#708.
When an actor pushes a change to a remote repository, this update to the
corresponding ref (or refs) must be recorded in the RSL. For each ref being
pushed, the gittuf client creates a new RSL entry. Then, RSLPush is used to
submit these changes to the remote repository.
Due to the linear nature of the RSL, it is not possible to remove a reference
entry. A force push makes one or more prior reference entries for the pushed ref
invalid as the targets recorded in those entries may not be reachable any
longer. Thus, these entries must be marked as "skipped" in the RSL using an
annotation entry. After an annotation for these reference entries is created, a
reference entry is created recording the current state of the ref. Then,
RSLPush is used to submit these changes to the remote repository.
There are several aspects to verification. First, the right policy state must be identified by walking back RSL entries to find the last change to that namespace. Next, authorized keys must be identified to verify that commit or RSL entry signatures are valid.
When verifying a commit or RSL entry, the first step is identifying the set of
keys authorized to sign a commit or RSL entry in their respective namespaces.
This is achieved by performing pre-ordered depth first search over the
delegations graph in a gittuf policy state. Assume the relevant policy state
entry is P and the namespace being checked is N. Then:
- Validate
P's root metadata using the TUF workflow starting from the initial root of trust metadata, ignore expiration date checks (see gittuf#280). - Create empty set
Kto record authorized verifiers forN. - Create empty set
queueto track the rules (or delegations) that must be checked. - Begin traversing the delegations graph rooted at the primary rule file metadata.
- Verify the signatures of the primary rule file using the trusted keys in the root of trust. If a threshold of signatures cannot be verified, abort.
- Populate
queuewith the rules in the primary rule file. - While
queueis not empty:- Set
ruleto the first item inqueue, removing it fromqueue. - If
ruleis theallow-rule:- Proceed to the next iteration.
- If the patterns of
rulematchN(i.e., the rule applies to the namespace being verified):- Create a verifier with the trusted keys in
ruleand the specified threshold. - Add this verifier to
K. - If
Pcontains a rule file with the same name asrule(i.e., a delegated rule file exists):- Verify that the delegated rule file is signed by a threshold of valid signatures using the keys declared in delegating rule file. Abort if verification fails.
- Add the rules in
currentto the front ofqueue(ensuring the delegated rules are prioritized to match pre-order depth first search behavior).
- Create a verifier with the trusted keys in
- Set
- Return
K.
In gittuf, verifying the validity of changes is relative. Verification of a
new state depends on comparing it against some prior, verified state. For some
ref X that is currently at verified entry S in the RSL and its latest
available state entry is D:
- Fetch all changes made to
Xbetween the commit recorded inSand that recorded inD, including the latest commit into a temporary branch. - Walk back from
Suntil an RSL entryPis found that updated the gittuf policy namespace. This identifies the policy that was active for changes made immediately afterS. If a policy entry is not found, abort. - Walk back from
Suntil an RSL entryAis found that updated the gittuf attestations ref. This identifies the set of attestations applicable for the changes made immediately afterS. - Validate
P's metadata using the TUF workflow, ignore expiration date checks (see gittuf#280). - Walk back from
DuntilSand create an ordered list of all RSL updates that targeted eitherXor gittuf namespaces. Entries pertaining to other refs MAY be ignored. Annotation entries MUST be recorded. - The verification workflow has an ordered list of states
[I1, I2, ..., In, D]that are to be verified. - Set trusted set for
XtoS. - For each set of consecutive states starting with
(S, I1)to(In, D):- Check if an annotation exists for the second state. If it does, verify if the annotation indicates the state is to be skipped. It true, proceed to the next set of consecutive states.
- If second state changes gittuf policy:
- Validate new policy metadata using the TUF workflow and
P's contents to established authorized signers for new policy. Ignore expiration date checks (see gittuf#280). If verification passes, updatePto new policy state.
- Validate new policy metadata using the TUF workflow and
- If second state is for attestations:
- Set
Ato the new attestations state.
- Set
- Verify the second state entry was signed by an authorized key as defined
in
Pfor the refX. If the gittuf policy requires more than one signature, search for a reference authorization attestation for the same change. Verify the signatures on the attestation are issued by authorized keys to meet the threshold, ignoring any signatures from the same key as the one used to sign the entry. - If
Pcontains rules protecting files in the repository:- Enumerate all commits between that recorded in trusted state and the second state with the signing key used for each commit.
- Identify the net or combined set of files modified between the commits
in the first and second states as
F. - If all commits are signed by the same key, individual commits need not
be validated. Instead,
Fcan be used directly. For each path:- Find the set of keys authorized to make changes to the path in
P. - Verify key used is in authorized set. If not, terminate verification workflow with an error.
- Find the set of keys authorized to make changes to the path in
- If not, iterate over each commit. For each commit:
- Identify the file paths modified by the commit. For each path:
- Find the set of keys authorized to make changes to the path in
P. - Verify key used is in authorized set. If not, check if path is
present in
F, as an unauthorized change may have been corrected subsequently. This merely acts as a hint as path may have been also changed subsequently by an authorized user, meaning it is inF. If path is not inF, continue with verification. Else, request user input, indicating potential policy violation.
- Find the set of keys authorized to make changes to the path in
- Identify the file paths modified by the commit. For each path:
- Set trusted state for
Xto second state of current iteration.
- Return indicating successful verification.
NOTE: Some aspects of this workflow are under discussion and are subject to change. The gittuf implementation does not implement precisely this workflow, instead also including aspects of the recovery workflow to see if a change that fails verification has already been recovered from. See gittuf#708.
If every user were using gittuf and were performing each operation by generating all of the correct metadata, following the specification, etc., then the procedure for handling each situation is fairly straightforward. However, an important property of gittuf is to ensure that a malicious or erroneous party cannot make changes that impact the state of the repository in a negative manner. To address this, this section discusses how to handle situations where something has not gone according to protocol. The goal is to recover to a "known good" situation which does match the metadata which a set of valid gittuf clients would generate.
When gittuf verification fails, the following recovery workflow must be employed. This mechanism is utilized in scenarios where some change is rejected. For example, one or more commits may have been pushed to a branch that do not meet gittuf policy. The repository is updated such that these commits are neutralized and all Git refs match their latest RSL entries. This can take two forms:
-
The rejected commit is removed and the state of the repo is set to the prior commit which is known to be good. This is used when all rejected commits are together at the end of the commit graph, making it easy to remove all of them.
-
The rejected commit is reverted where a new commit is introduced that reverses all the changes made in the reverted commit. This is needed when "good" commits that must be retained are interspersed with "bad" commits that must be rejected.
In both cases, new RSL entries and annotations must be used to record the incident and skip the invalid RSL entries corresponding to the rejected changes.
gittuf, by default, prefers the second option, with an explicit revert commit that is tree-same as the last good commit. This ensures that a client can always fast-forward to a fix rather than rewind. By resetting the affected branch to a prior good commit, Git clients that have already pulled in the invalid commit will not reset as well. Instead, they will assume they are ahead of the remote in question and will continue to use the bad commit as the latest commit.
When the gittuf verification workflow encounters an RSL entry for some Git reference that does not meet policy, it looks to see if a subsequent entry for the same reference contains a fix that aligns with the last known good state. Any intermediate entries between the original invalid entry and the fix for the reference in question are also considered to be invalid. Therefore, in addition to the fix RSL entry, gittuf also expects skip annotations for the original invalid entry and intermediate entries for the reference.
These scenarios are some examples where recovery is necessary. This is not meant to be an exhaustive set of gittuf's recovery scenarios.
There are several ways in which an RSL entry can be considered "incorrect". If an entry is malformed (structurally), Git may catch it if it's not a valid commit. In such instances, the push from a buggy client is rejected altogether, meaning other users are not exposed to the malformed commit.
Invalid entries that are not rejected by Git must be caught by gittuf. Some examples of such invalid entries are:
- RSL entry is for a non-existing Git reference
- Commit recorded in RSL entry does not exist
- Commit recorded in RSL entry does not match the tip of the corresponding Git reference
- RSL annotation contains references to RSL entries that do not exist or are not RSL entries (i.e. the annotation points to other commits in the repository)
Note that as invalid RSL entries are only created by buggy or malicious gittuf clients, these entries cannot be detected prior to them being pushed to the synchronization point.
As correctly implemented gittuf clients verify the validity of RSL entries when they pull from the synchronization point, the user is warned if invalid entries are encountered. Then, the user can then use the recovery workflow to invalidate the incorrect entry. Other clients with the invalid entry only need to fetch the latest RSL entries to recover. Additionally, the client that created the invalid entries must switch to a correct implementation of gittuf before further interactions with the main repository, but this is left to out-of-band synchronization between the actors who notice the issue and the actor using a buggy client.
An actor, Bob, creates an RSL entry for a branch he's not authorized for by gittuf policy. He pushes a change to that branch. Another actor, Alice, notices this when her gittuf client indicates a failure in the verification workflow. Alice creates an RSL annotation marking Bob's entry as one to be skipped. Alice also reverses Bob's change, creating a new RSL entry reflecting that.
Overwriting or deleting an historical RSL entry is a complicated proposition. Git's content addressable properties mean that a SHA-1 collision is necessary to overwrite an existing RSL entry in the Git object store. Further, the attacker also needs more than push access to the repository as Git will not accept an object it already has in its store. Similarly, deleting an entry from the object store preserves the RSL structure cosmetically but verification workflows that require the entry will fail. This ensures that such an attack is detected, at which point the owners of the repository can restore the RSL state from their local copies.
Also note that while Git uses SHA-1 for its object store, cryptographic signatures are generated and verified using stronger hash algorithms. Therefore, a successful SHA-1 collision for an RSL entry will not go undetected as all entries are signed.
An attacker who controls the forge may attempt a fork* attack where different developers receive different RSL states. For example, the attacker may drop a push from an actor, Alice, from the RSL. Other developers such as Bob and Carol would continue adding their RSL entries, unaware of the dropped entry. However, Alice will observe the divergence in the RSL as she cannot receive Bob's and Carol's changes.
The attacker cannot simply reapply Bob's and Carol's changes over Alice's RSL entry without also controlling Bob's and Carol's keys. The attacker may attempt a freeze attack targeted against Alice, where she's always told her entry is the latest in the RSL. However, any out-of-band communication between Alice and either Bob or Carol (common during development workflows) will highlight the attack.
When a key authorized by gittuf policy is compromised, it must be revoked and rotated so that an attacker cannot use it to sign repository objects. gittuf policies that grant permissions to the key must be updated to revoke the key, possibly adding the actor's new key in the process. Further, if a security analysis shows that the key was used to make malicious changes, those changes must be reverted and the corresponding RSL entries signed with the compromised key must be skipped. This ensures that gittuf clients do not consider attacker created RSL entries as valid states for the corresponding Git references. Clients that have an older RSL from before the attack can skip past the malicious entries altogether.
Consider project foo's Git repository maintained by Alice and Bob. Alice and
Bob are the only actors authorized to update the state of the main branch. This
is accomplished by defining a TUF delegation to Alice and Bob's keys for the
namespace corresponding to the main branch. All changes to the main branch's
state MUST have a corresponding entry in the repository's RSL signed by either
Alice or Bob.
Further, foo has another contributor, Clara, who does not have maintainer
privileges. This means that Clara is free to make changes to other Git branches
but only Alice or Bob may merge Clara's changes from other unprotected branches
into the main branch.
Over time, foo grows to incorporate several subprojects with other
contributors Dave and Ella. Alice and Bob take the decision to reorganize the
repository into a monorepo containing two projects, bar and baz. Clara and
Dave work exclusively on bar and Ella works on baz with Bob. In this situation,
Alice and Bob retain their privileges to merge changes to the main branch.
Further, they set up delegations for each subproject's path within the
repository. Clara and Dave are only authorized to work on files within bar/*
and Ella is restricted to baz/*. As Bob is a maintainer of foo, he is not
restricted to working only on baz/*.