VYPR
Medium severity6.0GHSA Advisory· Published May 14, 2026· Updated May 14, 2026

CVE-2026-44544

CVE-2026-44544

Description

gittuf is a platform-agnostic Git security system. Prior to 0.14.0, an attacker with push access to gittuf's Reference State Log (RSL) can roll back the current policy to any previous policy trusted by the current set of root keys. gittuf determines the policy to load by inspecting the RSL. Except for the very first policy (which is automatically trusted given gittuf's TOFU model, or verified against manually specified keys), whenever an RSL entry that points to a new policy is encountered, gittuf validates that this policy is trusted. This is done by checking that the new policy’s root metadata is signed by the required threshold of the current policy's root keys. Because of this, an attacker with push access to the RSL may create a new entry that references an old policy (that is trusted by the most recent policy's set of root keys), thereby rolling back gittuf's policy to the attacker's chosen state. This vulnerability is fixed in 0.14.0.

Affected products

1

Patches

1
dd76efa505f9

Merge pull request #1319 from gittuf/vuln-fixes

https://github.com/gittuf/gittufAditya Sirish A YelgundhalliMay 1, 2026via ghsa
26 files changed · +961 326
  • docs/cli/gittuf_policy_increment-version.md+35 0 added
    @@ -0,0 +1,35 @@
    +## gittuf policy increment-version
    +
    +Increment the integer version of the specified rule file metadata
    +
    +### Synopsis
    +
    +The 'increment-version' command increments the integer version of the specified rule file metadata without making any other changes.
    +
    +```
    +gittuf policy increment-version [flags]
    +```
    +
    +### Options
    +
    +```
    +  -h, --help                 help for increment-version
    +      --policy-name string   name of policy file to increment version of (default "targets")
    +```
    +
    +### Options inherited from parent commands
    +
    +```
    +      --create-rsl-entry             create RSL entry for policy change immediately (note: the RSL will not be synced with the remote)
    +      --no-color                     turn off colored output
    +      --profile                      enable CPU and memory profiling
    +      --profile-CPU-file string      file to store CPU profile (default "cpu.prof")
    +      --profile-memory-file string   file to store memory profile (default "memory.prof")
    +  -k, --signing-key string           signing key to use to sign root of trust (path to SSH key, "fulcio:" for Sigstore)
    +      --verbose                      enable verbose logging
    +```
    +
    +### SEE ALSO
    +
    +* [gittuf policy](gittuf_policy.md)	 - Tools to manage gittuf policies
    +
    
  • docs/cli/gittuf_policy.md+1 0 modified
    @@ -32,6 +32,7 @@ The 'policy' command provides a suite of tools for managing gittuf policy config
     * [gittuf policy add-rule](gittuf_policy_add-rule.md)	 - Add a new rule to a policy file
     * [gittuf policy apply](gittuf_policy_apply.md)	 - Validate and apply changes from policy-staging to policy
     * [gittuf policy discard](gittuf_policy_discard.md)	 - Discard the currently staged changes to policy
    +* [gittuf policy increment-version](gittuf_policy_increment-version.md)	 - Increment the integer version of the specified rule file metadata
     * [gittuf policy init](gittuf_policy_init.md)	 - Initialize policy file
     * [gittuf policy list-principals](gittuf_policy_list-principals.md)	 - List principals for the current policy in the specified rule file
     * [gittuf policy list-rules](gittuf_policy_list-rules.md)	 - List rules for the current state
    
  • docs/cli/gittuf_trust_increment-version.md+34 0 added
    @@ -0,0 +1,34 @@
    +## gittuf trust increment-version
    +
    +Increment the integer version of the root metadata
    +
    +### Synopsis
    +
    +The 'increment-version' command increments the integer version of the root metadata without making any other changes.
    +
    +```
    +gittuf trust increment-version [flags]
    +```
    +
    +### Options
    +
    +```
    +  -h, --help   help for increment-version
    +```
    +
    +### Options inherited from parent commands
    +
    +```
    +      --create-rsl-entry             create RSL entry for policy change immediately (note: the RSL will not be synced with the remote)
    +      --no-color                     turn off colored output
    +      --profile                      enable CPU and memory profiling
    +      --profile-CPU-file string      file to store CPU profile (default "cpu.prof")
    +      --profile-memory-file string   file to store memory profile (default "memory.prof")
    +  -k, --signing-key string           signing key to use to sign root of trust (path to SSH key, "fulcio:" for Sigstore)
    +      --verbose                      enable verbose logging
    +```
    +
    +### SEE ALSO
    +
    +* [gittuf trust](gittuf_trust.md)	 - Tools for gittuf's root of trust
    +
    
  • docs/cli/gittuf_trust.md+1 0 modified
    @@ -38,6 +38,7 @@ The 'trust' command provides tools to manage gittuf's root of trust, including s
     * [gittuf trust apply](gittuf_trust_apply.md)	 - Validate and apply changes from policy-staging to policy
     * [gittuf trust disable-github-app-approvals](gittuf_trust_disable-github-app-approvals.md)	 - Mark GitHub app approvals as untrusted henceforth
     * [gittuf trust enable-github-app-approvals](gittuf_trust_enable-github-app-approvals.md)	 - Mark GitHub app approvals as trusted henceforth
    +* [gittuf trust increment-version](gittuf_trust_increment-version.md)	 - Increment the integer version of the root metadata
     * [gittuf trust init](gittuf_trust_init.md)	 - Initialize gittuf root of trust for repository
     * [gittuf trust inspect-root](gittuf_trust_inspect-root.md)	 - Inspect root metadata
     * [gittuf trust list-global-rules](gittuf_trust_list-global-rules.md)	 - List global rules for the current state
    
  • experimental/gittuf/root.go+38 7 modified
    @@ -6,9 +6,7 @@ package gittuf
     import (
     	"context"
     	"crypto/sha256"
    -	"encoding/base64"
     	"encoding/hex"
    -	"encoding/json"
     	"errors"
     	"fmt"
     	"log/slog"
    @@ -900,6 +898,41 @@ func (r *Repository) UpdatePropagationDirective(ctx context.Context, signer ssli
     	return r.updateRootMetadata(ctx, state, signer, rootMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
    +func (r *Repository) IncrementRootVersion(ctx context.Context, signer sslibdsse.SignerVerifier, signCommit bool, opts ...trustpolicyopts.Option) error {
    +	if signCommit {
    +		slog.Debug("Checking if Git signing is configured...")
    +		err := r.r.CanSign()
    +		if err != nil {
    +			return err
    +		}
    +	}
    +
    +	options := &trustpolicyopts.Options{}
    +	for _, fn := range opts {
    +		fn(options)
    +	}
    +
    +	rootKeyID, err := signer.KeyID()
    +	if err != nil {
    +		return err
    +	}
    +
    +	slog.Debug("Loading current policy...")
    +	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef)
    +	if err != nil {
    +		return err
    +	}
    +
    +	rootMetadata, err := r.loadRootMetadata(state, rootKeyID)
    +	if err != nil {
    +		return err
    +	}
    +
    +	// Just pass it to updateRootMetadata as it will increment the version
    +	commitMessage := "Increment root version"
    +	return r.updateRootMetadata(ctx, state, signer, rootMetadata, commitMessage, options.CreateRSLEntry, signCommit)
    +}
    +
     func (r *Repository) RemovePropagationDirective(ctx context.Context, signer sslibdsse.SignerVerifier, name string, signCommit bool, opts ...trustpolicyopts.Option) error {
     	if signCommit {
     		slog.Debug("Checking if Git signing is configured...")
    @@ -1388,15 +1421,13 @@ func (r *Repository) loadRootMetadata(state *policy.State, keyID string) (tuf.Ro
     }
     
     func (r *Repository) updateRootMetadata(ctx context.Context, state *policy.State, signer sslibdsse.SignerVerifier, rootMetadata tuf.RootMetadata, commitMessage string, createRSLEntry, signCommit bool) error {
    -	rootMetadataBytes, err := json.Marshal(rootMetadata)
    +	rootMetadata.IncrementVersion()
    +
    +	env, err := dsse.CreateEnvelope(rootMetadata)
     	if err != nil {
     		return err
     	}
     
    -	env := state.Metadata.RootEnvelope
    -	env.Signatures = []sslibdsse.Signature{}
    -	env.Payload = base64.StdEncoding.EncodeToString(rootMetadataBytes)
    -
     	slog.Debug("Signing updated root metadata...")
     	env, err = dsse.SignEnvelope(ctx, env, signer)
     	if err != nil {
    
  • experimental/gittuf/root_test.go+32 0 modified
    @@ -2520,6 +2520,38 @@ func TestUpdateHook(t *testing.T) {
     	})
     }
     
    +func TestIncrementRootVersion(t *testing.T) {
    +	r := createTestRepositoryWithRoot(t, "")
    +
    +	state, err := policy.LoadCurrentState(testCtx, r.r, policy.PolicyRef)
    +	require.Nil(t, err)
    +
    +	oldRootMetadata, err := state.GetRootMetadata(false)
    +	require.Nil(t, err)
    +
    +	rootSigner := setupSSHKeysForSigning(t, rootKeyBytes, rootPubKeyBytes)
    +
    +	err = r.IncrementRootVersion(testCtx, rootSigner, false)
    +	require.Nil(t, err)
    +
    +	err = r.StagePolicy(testCtx, "", true, false)
    +	require.Nil(t, err)
    +
    +	state, err = policy.LoadCurrentState(testCtx, r.r, policy.PolicyStagingRef)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	newRootMetadata, err := state.GetRootMetadata(false)
    +	require.Nil(t, err)
    +
    +	assert.Equal(t, oldRootMetadata.GetVersion()+1, newRootMetadata.GetVersion())
    +
    +	// Check that the metadata is the same except for the version number
    +	newRootMetadata.(*tufv02.RootMetadata).Version = oldRootMetadata.GetVersion()
    +	assert.Equal(t, oldRootMetadata, newRootMetadata)
    +}
    +
     func TestListPropagationDirectives(t *testing.T) {
     	t.Run("list propagation directives after add and remove", func(t *testing.T) {
     		r := createTestRepositoryWithRoot(t, "")
    
  • experimental/gittuf/targets.go+66 181 modified
    @@ -40,11 +40,6 @@ func (r *Repository) InitializeTargets(ctx context.Context, signer sslibdsse.Sig
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -67,7 +62,7 @@ func (r *Repository) InitializeTargets(ctx context.Context, signer sslibdsse.Sig
     		return err
     	}
     
    -	slog.Debug(fmt.Sprintf("Signing initial rule file using '%s'...", keyID))
    +	slog.Debug("Signing initial rule file...")
     	env, err = dsse.SignEnvelope(ctx, env, signer)
     	if err != nil {
     		return err
    @@ -108,11 +103,6 @@ func (r *Repository) AddDelegation(ctx context.Context, signer sslibdsse.SignerV
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -144,27 +134,8 @@ func (r *Repository) AddDelegation(ctx context.Context, signer sslibdsse.SignerV
     		return err
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Add rule '%s' to policy '%s'", ruleName, targetsRoleName)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // UpdateDelegation is the interface for the user to update a rule to gittuf
    @@ -187,11 +158,6 @@ func (r *Repository) UpdateDelegation(ctx context.Context, signer sslibdsse.Sign
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -218,27 +184,8 @@ func (r *Repository) UpdateDelegation(ctx context.Context, signer sslibdsse.Sign
     		return err
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Update rule '%s' in policy '%s'", ruleName, targetsRoleName)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // ReorderDelegations is the interface for the user to reorder rules in gittuf
    @@ -257,11 +204,6 @@ func (r *Repository) ReorderDelegations(ctx context.Context, signer sslibdsse.Si
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return nil
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -283,27 +225,8 @@ func (r *Repository) ReorderDelegations(ctx context.Context, signer sslibdsse.Si
     		return err
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Reorder rules in policy '%s'", targetsRoleName)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // RemoveDelegation is the interface for a user to remove a rule from gittuf
    @@ -322,11 +245,6 @@ func (r *Repository) RemoveDelegation(ctx context.Context, signer sslibdsse.Sign
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -353,27 +271,8 @@ func (r *Repository) RemoveDelegation(ctx context.Context, signer sslibdsse.Sign
     		return err
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Remove rule '%s' from policy '%s'", ruleName, targetsRoleName)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // AddPrincipalToTargets is the interface for a user to add a trusted principal
    @@ -392,11 +291,6 @@ func (r *Repository) AddPrincipalToTargets(ctx context.Context, signer sslibdsse
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -429,27 +323,8 @@ func (r *Repository) AddPrincipalToTargets(ctx context.Context, signer sslibdsse
     		}
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Add principals to policy '%s'\n%s", targetsRoleName, principalIDs)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // UpdatePrincipalInTargets is the interface for a user to update a principal's
    @@ -468,11 +343,6 @@ func (r *Repository) UpdatePrincipalInTargets(ctx context.Context, signer sslibd
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -493,27 +363,8 @@ func (r *Repository) UpdatePrincipalInTargets(ctx context.Context, signer sslibd
     		return err
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Update principal '%s' in policy '%s'", principal.ID(), targetsRoleName)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // RemovePrincipalFromTargets is the interface for a user to remove a principal
    @@ -532,11 +383,6 @@ func (r *Repository) RemovePrincipalFromTargets(ctx context.Context, signer ssli
     		fn(options)
     	}
     
    -	keyID, err := signer.KeyID()
    -	if err != nil {
    -		return err
    -	}
    -
     	slog.Debug("Loading current policy...")
     	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
     	if err != nil {
    @@ -557,27 +403,8 @@ func (r *Repository) RemovePrincipalFromTargets(ctx context.Context, signer ssli
     		return err
     	}
     
    -	env, err := dsse.CreateEnvelope(targetsMetadata)
    -	if err != nil {
    -		return err
    -	}
    -
    -	slog.Debug(fmt.Sprintf("Signing updated rule file using '%s'...", keyID))
    -	env, err = dsse.SignEnvelope(ctx, env, signer)
    -	if err != nil {
    -		return err
    -	}
    -
    -	if targetsRoleName == policy.TargetsRoleName {
    -		state.Metadata.TargetsEnvelope = env
    -	} else {
    -		state.Metadata.DelegationEnvelopes[targetsRoleName] = env
    -	}
    -
     	commitMessage := fmt.Sprintf("Remove principal from policy '%s'\n%s", targetsRoleName, principalID)
    -
    -	slog.Debug("Committing policy...")
    -	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
     }
     
     // SignTargets adds a signature to specified Targets role's envelope. Note that
    @@ -634,3 +461,61 @@ func (r *Repository) SignTargets(ctx context.Context, signer sslibdsse.SignerVer
     	slog.Debug("Committing policy...")
     	return state.Commit(r.r, commitMessage, options.CreateRSLEntry, signCommit)
     }
    +
    +func (r *Repository) IncrementTargetsVersion(ctx context.Context, signer sslibdsse.SignerVerifier, targetsRoleName string, signCommit bool, opts ...trustpolicyopts.Option) error {
    +	if signCommit {
    +		slog.Debug("Checking if Git signing is configured...")
    +		err := r.r.CanSign()
    +		if err != nil {
    +			return err
    +		}
    +	}
    +
    +	options := &trustpolicyopts.Options{}
    +	for _, fn := range opts {
    +		fn(options)
    +	}
    +
    +	slog.Debug("Loading current policy...")
    +	state, err := policy.LoadCurrentState(ctx, r.r, policy.PolicyStagingRef, policyopts.BypassRSL())
    +	if err != nil {
    +		return err
    +	}
    +	if !state.HasTargetsRole(targetsRoleName) {
    +		return policy.ErrMetadataNotFound
    +	}
    +
    +	slog.Debug("Loading current rule file...")
    +	targetsMetadata, err := state.GetTargetsMetadata(targetsRoleName, true)
    +	if err != nil {
    +		return err
    +	}
    +
    +	// Just pass it to updateTargetsMetadata as it will increment the version
    +	commitMessage := fmt.Sprintf("Increment rule file '%s' version", targetsRoleName)
    +	return r.updateTargetsMetadata(ctx, state, signer, targetsRoleName, targetsMetadata, commitMessage, options.CreateRSLEntry, signCommit)
    +}
    +
    +func (r *Repository) updateTargetsMetadata(ctx context.Context, state *policy.State, signer sslibdsse.SignerVerifier, targetsMetadataName string, targetsMetadata tuf.TargetsMetadata, commitMessage string, createRSLEntry, signCommit bool) error {
    +	targetsMetadata.IncrementVersion()
    +
    +	env, err := dsse.CreateEnvelope(targetsMetadata)
    +	if err != nil {
    +		return err
    +	}
    +
    +	slog.Debug("Signing updated rule file...")
    +	env, err = dsse.SignEnvelope(ctx, env, signer)
    +	if err != nil {
    +		return err
    +	}
    +
    +	if targetsMetadataName == policy.TargetsRoleName {
    +		state.Metadata.TargetsEnvelope = env
    +	} else {
    +		state.Metadata.DelegationEnvelopes[targetsMetadataName] = env
    +	}
    +
    +	slog.Debug("Committing policy...")
    +	return state.Commit(r.r, commitMessage, createRSLEntry, signCommit)
    +}
    
  • experimental/gittuf/targets_test.go+32 0 modified
    @@ -434,3 +434,35 @@ func TestSignTargets(t *testing.T) {
     
     	assert.Equal(t, 2, len(state.Metadata.TargetsEnvelope.Signatures))
     }
    +
    +func TestIncrementTargetsVersion(t *testing.T) {
    +	r := createTestRepositoryWithPolicy(t, "")
    +
    +	state, err := policy.LoadCurrentState(testCtx, r.r, policy.PolicyRef)
    +	require.Nil(t, err)
    +
    +	oldTargetsMetadata, err := state.GetTargetsMetadata(policy.TargetsRoleName, false)
    +	require.Nil(t, err)
    +
    +	targetsSigner := setupSSHKeysForSigning(t, targetsKeyBytes, targetsPubKeyBytes)
    +
    +	err = r.IncrementTargetsVersion(testCtx, targetsSigner, policy.TargetsRoleName, false)
    +	assert.Nil(t, err)
    +
    +	err = r.StagePolicy(testCtx, "", true, false)
    +	require.Nil(t, err)
    +
    +	state, err = policy.LoadCurrentState(testCtx, r.r, policy.PolicyStagingRef)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	newTargetsMetadata, err := state.GetTargetsMetadata(policy.TargetsRoleName, false)
    +	require.Nil(t, err)
    +
    +	assert.Equal(t, oldTargetsMetadata.GetVersion()+1, newTargetsMetadata.GetVersion())
    +
    +	// Check that the metadata is the same except for the version number
    +	newTargetsMetadata.(*tufv02.TargetsMetadata).Version = oldTargetsMetadata.GetVersion()
    +	assert.Equal(t, oldTargetsMetadata, newTargetsMetadata)
    +}
    
  • internal/cmd/policy/incrementversion/incrementversion.go+58 0 added
    @@ -0,0 +1,58 @@
    +// Copyright The gittuf Authors
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package incrementversion
    +
    +import (
    +	"github.com/gittuf/gittuf/experimental/gittuf"
    +	trustpolicyopts "github.com/gittuf/gittuf/experimental/gittuf/options/trustpolicy"
    +	"github.com/gittuf/gittuf/internal/cmd/policy/persistent"
    +	"github.com/gittuf/gittuf/internal/policy"
    +	"github.com/spf13/cobra"
    +)
    +
    +type options struct {
    +	p          *persistent.Options
    +	policyName string
    +}
    +
    +func (o *options) AddFlags(cmd *cobra.Command) {
    +	cmd.Flags().StringVar(
    +		&o.policyName,
    +		"policy-name",
    +		policy.TargetsRoleName,
    +		"name of policy file to increment version of",
    +	)
    +}
    +
    +func (o *options) Run(cmd *cobra.Command, _ []string) error {
    +	repo, err := gittuf.LoadRepository(".")
    +	if err != nil {
    +		return err
    +	}
    +
    +	signer, err := gittuf.LoadSigner(repo, o.p.SigningKey)
    +	if err != nil {
    +		return err
    +	}
    +
    +	opts := []trustpolicyopts.Option{}
    +	if o.p.WithRSLEntry {
    +		opts = append(opts, trustpolicyopts.WithRSLEntry())
    +	}
    +	return repo.IncrementTargetsVersion(cmd.Context(), signer, o.policyName, true, opts...)
    +}
    +
    +func New(persistent *persistent.Options) *cobra.Command {
    +	o := &options{p: persistent}
    +	cmd := &cobra.Command{
    +		Use:               "increment-version",
    +		Short:             "Increment the integer version of the specified rule file metadata",
    +		Long:              `The 'increment-version' command increments the integer version of the specified rule file metadata without making any other changes.`,
    +		RunE:              o.Run,
    +		DisableAutoGenTag: true,
    +	}
    +	o.AddFlags(cmd)
    +
    +	return cmd
    +}
    
  • internal/cmd/policy/policy.go+2 0 modified
    @@ -7,6 +7,7 @@ import (
     	"github.com/gittuf/gittuf/internal/cmd/policy/addkey"
     	"github.com/gittuf/gittuf/internal/cmd/policy/addperson"
     	"github.com/gittuf/gittuf/internal/cmd/policy/addrule"
    +	"github.com/gittuf/gittuf/internal/cmd/policy/incrementversion"
     	i "github.com/gittuf/gittuf/internal/cmd/policy/init"
     	"github.com/gittuf/gittuf/internal/cmd/policy/listprincipals"
     	"github.com/gittuf/gittuf/internal/cmd/policy/listrules"
    @@ -41,6 +42,7 @@ func New() *cobra.Command {
     	cmd.AddCommand(apply.New())
     	cmd.AddCommand(discard.New())
     	cmd.AddCommand(i.New(o))
    +	cmd.AddCommand(incrementversion.New(o))
     	cmd.AddCommand(listprincipals.New())
     	cmd.AddCommand(listrules.New())
     	cmd.AddCommand(remote.New())
    
  • internal/cmd/trust/incrementversion/incrementversion.go+46 0 added
    @@ -0,0 +1,46 @@
    +// Copyright The gittuf Authors
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package incrementversion
    +
    +import (
    +	"github.com/gittuf/gittuf/experimental/gittuf"
    +	trustpolicyopts "github.com/gittuf/gittuf/experimental/gittuf/options/trustpolicy"
    +	"github.com/gittuf/gittuf/internal/cmd/trust/persistent"
    +	"github.com/spf13/cobra"
    +)
    +
    +type options struct {
    +	p *persistent.Options
    +}
    +
    +func (o *options) Run(cmd *cobra.Command, _ []string) error {
    +	repo, err := gittuf.LoadRepository(".")
    +	if err != nil {
    +		return err
    +	}
    +
    +	signer, err := gittuf.LoadSigner(repo, o.p.SigningKey)
    +	if err != nil {
    +		return err
    +	}
    +
    +	opts := []trustpolicyopts.Option{}
    +	if o.p.WithRSLEntry {
    +		opts = append(opts, trustpolicyopts.WithRSLEntry())
    +	}
    +	return repo.IncrementRootVersion(cmd.Context(), signer, true, opts...)
    +}
    +
    +func New(persistent *persistent.Options) *cobra.Command {
    +	o := &options{p: persistent}
    +	cmd := &cobra.Command{
    +		Use:               "increment-version",
    +		Short:             "Increment the integer version of the root metadata",
    +		Long:              `The 'increment-version' command increments the integer version of the root metadata without making any other changes.`,
    +		RunE:              o.Run,
    +		DisableAutoGenTag: true,
    +	}
    +
    +	return cmd
    +}
    
  • internal/cmd/trust/trust.go+2 0 modified
    @@ -14,6 +14,7 @@ import (
     	"github.com/gittuf/gittuf/internal/cmd/trust/addrootkey"
     	"github.com/gittuf/gittuf/internal/cmd/trust/disablegithubappapprovals"
     	"github.com/gittuf/gittuf/internal/cmd/trust/enablegithubappapprovals"
    +	"github.com/gittuf/gittuf/internal/cmd/trust/incrementversion"
     	i "github.com/gittuf/gittuf/internal/cmd/trust/init"
     	"github.com/gittuf/gittuf/internal/cmd/trust/inspectroot"
     	"github.com/gittuf/gittuf/internal/cmd/trust/listglobalrules"
    @@ -62,6 +63,7 @@ func New() *cobra.Command {
     	cmd.AddCommand(apply.New())
     	cmd.AddCommand(disablegithubappapprovals.New(o))
     	cmd.AddCommand(enablegithubappapprovals.New(o))
    +	cmd.AddCommand(incrementversion.New(o))
     	cmd.AddCommand(inspectroot.New())
     	cmd.AddCommand(listglobalrules.New())
     	cmd.AddCommand(listhooks.New())
    
  • internal/policy/helpers_test.go+177 0 modified
    @@ -171,6 +171,42 @@ func createTestStateWithOnlyRoot(t *testing.T) *State {
     	}
     }
     
    +func createTestStateWithOnlyRootUnnumbered(t *testing.T) *State {
    +	// This is a clone of createTestStateWithOnlyRoot but with the version of
    +	// the root metadata overridden to be 0 (unnumbered). This allows us to test
    +	// the behavior of transitioning from unnumbered to numbered metadata, which
    +	// is relevant for repositories that had existing metadata before gittuf was
    +	// introduced.
    +
    +	t.Helper()
    +
    +	signer := setupSSHKeysForSigning(t, rootKeyBytes, rootPubKeyBytes) //nolint:staticcheck
    +	key := tufv01.NewKeyFromSSLibKey(signer.MetadataKey())
    +
    +	rootMetadata, err := InitializeRootMetadata(key)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	// Override the version to be 0
    +	rootMetadata.(*tufv02.RootMetadata).Version = 0
    +
    +	rootEnv, err := dsse.CreateEnvelope(rootMetadata)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	rootEnv, err = dsse.SignEnvelope(context.Background(), rootEnv, signer)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	return &State{
    +		Metadata: &StateMetadata{
    +			RootEnvelope: rootEnv,
    +		},
    +	}
    +}
    +
     func createTestStateWithPolicy(t *testing.T) *State {
     	t.Helper()
     
    @@ -236,6 +272,147 @@ func createTestStateWithPolicy(t *testing.T) *State {
     	return state
     }
     
    +func createTestStateWithPolicyUnnumbered(t *testing.T) *State {
    +	// This is a clone of createTestStateWithPolicy but with the version of the
    +	// metadata overridden to be 0 (unnumbered). This allows us to test the
    +	// behavior of transitioning from unnumbered to numbered metadata, which is
    +	// relevant for repositories that had existing metadata before gittuf was
    +	// introduced.
    +	t.Helper()
    +
    +	signer := setupSSHKeysForSigning(t, rootKeyBytes, rootPubKeyBytes)
    +	key := tufv01.NewKeyFromSSLibKey(signer.MetadataKey())
    +
    +	rootMetadata, err := InitializeRootMetadata(key)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	// Override the version to be 0
    +	rootMetadata.(*tufv02.RootMetadata).Version = 0
    +
    +	if err := rootMetadata.AddPrimaryRuleFilePrincipal(key); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	rootEnv, err := dsse.CreateEnvelope(rootMetadata)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	rootEnv, err = dsse.SignEnvelope(context.Background(), rootEnv, signer)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	gpgKeyR, err := gpg.LoadGPGKeyFromBytes(gpgPubKeyBytes)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	gpgKey := tufv01.NewKeyFromSSLibKey(gpgKeyR)
    +
    +	targetsMetadata := InitializeTargetsMetadata()
    +	if err := targetsMetadata.AddPrincipal(gpgKey); err != nil {
    +		t.Fatal(err)
    +	}
    +	if err := targetsMetadata.AddRule("protect-main", []string{gpgKey.KeyID}, []string{"git:refs/heads/main"}, 1); err != nil {
    +		t.Fatal(err)
    +	}
    +	// Add a file protection rule. When used with common.AddNTestCommitsToSpecifiedRef, we have files with names 1, 2, 3,...n.
    +	if err := targetsMetadata.AddRule("protect-files-1-and-2", []string{gpgKey.KeyID}, []string{"file:1", "file:2"}, 1); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	// Override the version to be 0
    +	targetsMetadata.(*tufv02.TargetsMetadata).Version = 0
    +
    +	targetsEnv, err := dsse.CreateEnvelope(targetsMetadata)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	targetsEnv, err = dsse.SignEnvelope(context.Background(), targetsEnv, signer)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	state := &State{
    +		Metadata: &StateMetadata{
    +			RootEnvelope:    rootEnv,
    +			TargetsEnvelope: targetsEnv,
    +		},
    +	}
    +
    +	if err := state.preprocess(); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	return state
    +}
    +
    +func createTestStateWithPolicyTargetsSignedByWrongKey(t *testing.T) *State {
    +	rootSigner := setupSSHKeysForSigning(t, rootKeyBytes, rootPubKeyBytes)
    +	key := tufv01.NewKeyFromSSLibKey(rootSigner.MetadataKey())
    +
    +	rootMetadata, err := InitializeRootMetadata(key)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	if err := rootMetadata.AddPrimaryRuleFilePrincipal(key); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	rootEnv, err := dsse.CreateEnvelope(rootMetadata)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	rootEnv, err = dsse.SignEnvelope(context.Background(), rootEnv, rootSigner)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	gpgKeyR, err := gpg.LoadGPGKeyFromBytes(gpgPubKeyBytes)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	gpgKey := tufv01.NewKeyFromSSLibKey(gpgKeyR)
    +
    +	targetsSigner := setupSSHKeysForSigning(t, targets1KeyBytes, targets1PubKeyBytes)
    +
    +	targetsMetadata := InitializeTargetsMetadata()
    +	if err := targetsMetadata.AddPrincipal(gpgKey); err != nil {
    +		t.Fatal(err)
    +	}
    +	if err := targetsMetadata.AddRule("protect-main", []string{gpgKey.KeyID}, []string{"git:refs/heads/main"}, 1); err != nil {
    +		t.Fatal(err)
    +	}
    +	// Add a file protection rule. When used with common.AddNTestCommitsToSpecifiedRef, we have files with names 1, 2, 3,...n.
    +	if err := targetsMetadata.AddRule("protect-files-1-and-2", []string{gpgKey.KeyID}, []string{"file:1", "file:2"}, 1); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	targetsEnv, err := dsse.CreateEnvelope(targetsMetadata)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	targetsEnv, err = dsse.SignEnvelope(context.Background(), targetsEnv, targetsSigner)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	state := &State{
    +		Metadata: &StateMetadata{
    +			RootEnvelope:    rootEnv,
    +			TargetsEnvelope: targetsEnv,
    +		},
    +	}
    +
    +	if err := state.preprocess(); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	return state
    +}
    +
     // createTestStateWithGlobalConstraintThreshold creates a policy state with no
     // explicit branch protection rules but with a two-approval constraint on
     // changes to the main branch. The two keys trusted are `rootPubKeyBytes` and
    
  • internal/policy/policy.go+113 105 modified
    @@ -80,6 +80,112 @@ type StateMetadata struct {
     	DelegationEnvelopes map[string]*sslibdsse.Envelope
     }
     
    +func (s *StateMetadata) GetRootMetadata(migrate bool) (tuf.RootMetadata, error) {
    +	payloadBytes, err := s.RootEnvelope.DecodeB64Payload()
    +	if err != nil {
    +		return nil, err
    +	}
    +	return s.getRootMetadataFromBytes(payloadBytes, migrate)
    +}
    +
    +func (s *StateMetadata) getRootMetadataFromBytes(metadataBytes []byte, migrate bool) (tuf.RootMetadata, error) {
    +	inspectRootMetadata := map[string]any{}
    +	if err := json.Unmarshal(metadataBytes, &inspectRootMetadata); err != nil {
    +		return nil, fmt.Errorf("unable to unmarshal root metadata: %w", err)
    +	}
    +
    +	schemaVersion, hasSchemaVersion := inspectRootMetadata["schemaVersion"]
    +	switch {
    +	case !hasSchemaVersion:
    +		// this is tufv01
    +		// Something that's not tufv01 may also lack the schemaVersion field and
    +		// enter this code path. At that point, we're relying on the unmarshal
    +		// to return something that's close to tufv01. We may see strange bugs
    +		// if this happens, but it's also likely someone trying to submit
    +		// incorrect metadata / trigger a version rollback, which we do want to
    +		// be aware of.
    +		rootMetadata := &tufv01.RootMetadata{}
    +		if err := json.Unmarshal(metadataBytes, rootMetadata); err != nil {
    +			return nil, fmt.Errorf("unable to unmarshal root metadata: %w", err)
    +		}
    +
    +		if migrate {
    +			return migrations.MigrateRootMetadataV01ToV02(rootMetadata), nil
    +		}
    +
    +		return rootMetadata, nil
    +
    +	case schemaVersion == tufv02.RootVersion:
    +		rootMetadata := &tufv02.RootMetadata{}
    +		if err := json.Unmarshal(metadataBytes, rootMetadata); err != nil {
    +			return nil, fmt.Errorf("unable to unmarshal root metadata: %w", err)
    +		}
    +
    +		return rootMetadata, nil
    +
    +	default:
    +		return nil, tuf.ErrUnknownRootMetadataVersion
    +	}
    +}
    +
    +func (s *StateMetadata) GetTargetsMetadata(roleName string, migrate bool) (tuf.TargetsMetadata, error) {
    +	e := s.TargetsEnvelope
    +	if roleName != TargetsRoleName {
    +		env, ok := s.DelegationEnvelopes[roleName]
    +		if !ok {
    +			return nil, ErrMetadataNotFound
    +		}
    +		e = env
    +	}
    +
    +	if e == nil {
    +		return nil, ErrMetadataNotFound
    +	}
    +
    +	payloadBytes, err := e.DecodeB64Payload()
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	inspectTargetsMetadata := map[string]any{}
    +	if err := json.Unmarshal(payloadBytes, &inspectTargetsMetadata); err != nil {
    +		return nil, fmt.Errorf("unable to unmarshal rule file metadata: %w", err)
    +	}
    +
    +	schemaVersion, hasSchemaVersion := inspectTargetsMetadata["schemaVersion"]
    +	switch {
    +	case !hasSchemaVersion:
    +		// this is tufv01
    +		// Something that's not tufv01 may also lack the schemaVersion field and
    +		// enter this code path. At that point, we're relying on the unmarshal
    +		// to return something that's close to tufv01. We may see strange bugs
    +		// if this happens, but it's also likely someone trying to submit
    +		// incorrect metadata / trigger a version rollback, which we do want to
    +		// be aware of.
    +		targetsMetadata := &tufv01.TargetsMetadata{}
    +		if err := json.Unmarshal(payloadBytes, targetsMetadata); err != nil {
    +			return nil, fmt.Errorf("unable to unmarshal rule file metadata: %w", err)
    +		}
    +
    +		if migrate {
    +			return migrations.MigrateTargetsMetadataV01ToV02(targetsMetadata), nil
    +		}
    +
    +		return targetsMetadata, nil
    +
    +	case schemaVersion == tufv02.TargetsVersion:
    +		targetsMetadata := &tufv02.TargetsMetadata{}
    +		if err := json.Unmarshal(payloadBytes, targetsMetadata); err != nil {
    +			return nil, fmt.Errorf("unable to unmarshal rule file metadata: %w", err)
    +		}
    +
    +		return targetsMetadata, nil
    +
    +	default:
    +		return nil, tuf.ErrUnknownTargetsMetadataVersion
    +	}
    +}
    +
     func (s *StateMetadata) WriteTree(repo *gitinterface.Repository) (gitinterface.Hash, error) {
     	metadata := map[string]*sslibdsse.Envelope{}
     	metadata[RootRoleName] = s.RootEnvelope
    @@ -152,6 +258,10 @@ func LoadState(ctx context.Context, repo *gitinterface.Repository, requestedEntr
     			return nil, err
     		}
     
    +		if err := state.Verify(ctx); err != nil {
    +			return nil, fmt.Errorf("requested state has invalidly signed metadata: %w", err)
    +		}
    +
     		if len(options.InitialRootPrincipals) == 0 {
     			slog.Debug(fmt.Sprintf("Trusting root of trust for initial policy '%s'...", firstPolicyEntry.GetID().String()))
     			return state, nil
    @@ -975,11 +1085,7 @@ func (s *State) GetRootKeys() ([]tuf.Principal, error) {
     // The `migrate` parameter determines if the schema must be converted to a newer
     // version.
     func (s *State) GetRootMetadata(migrate bool) (tuf.RootMetadata, error) {
    -	payloadBytes, err := s.Metadata.RootEnvelope.DecodeB64Payload()
    -	if err != nil {
    -		return nil, err
    -	}
    -	return s.getRootMetadataFromBytes(payloadBytes, migrate)
    +	return s.Metadata.GetRootMetadata(migrate)
     }
     
     func (s *State) GetControllerRootMetadata(controllerName string) (tuf.RootMetadata, error) {
    @@ -988,112 +1094,14 @@ func (s *State) GetControllerRootMetadata(controllerName string) (tuf.RootMetada
     		return nil, fmt.Errorf("%w: '%s'", ErrControllerMetadataNotFound, controllerName)
     	}
     
    -	payloadBytes, err := metadata.RootEnvelope.DecodeB64Payload()
    -	if err != nil {
    -		return nil, err
    -	}
    -	return s.getRootMetadataFromBytes(payloadBytes, false) // never migrate
    -}
    -
    -func (s *State) getRootMetadataFromBytes(metadataBytes []byte, migrate bool) (tuf.RootMetadata, error) {
    -	inspectRootMetadata := map[string]any{}
    -	if err := json.Unmarshal(metadataBytes, &inspectRootMetadata); err != nil {
    -		return nil, fmt.Errorf("unable to unmarshal root metadata: %w", err)
    -	}
    -
    -	schemaVersion, hasSchemaVersion := inspectRootMetadata["schemaVersion"]
    -	switch {
    -	case !hasSchemaVersion:
    -		// this is tufv01
    -		// Something that's not tufv01 may also lack the schemaVersion field and
    -		// enter this code path. At that point, we're relying on the unmarshal
    -		// to return something that's close to tufv01. We may see strange bugs
    -		// if this happens, but it's also likely someone trying to submit
    -		// incorrect metadata / trigger a version rollback, which we do want to
    -		// be aware of.
    -		rootMetadata := &tufv01.RootMetadata{}
    -		if err := json.Unmarshal(metadataBytes, rootMetadata); err != nil {
    -			return nil, fmt.Errorf("unable to unmarshal root metadata: %w", err)
    -		}
    -
    -		if migrate {
    -			return migrations.MigrateRootMetadataV01ToV02(rootMetadata), nil
    -		}
    -
    -		return rootMetadata, nil
    -
    -	case schemaVersion == tufv02.RootVersion:
    -		rootMetadata := &tufv02.RootMetadata{}
    -		if err := json.Unmarshal(metadataBytes, rootMetadata); err != nil {
    -			return nil, fmt.Errorf("unable to unmarshal root metadata: %w", err)
    -		}
    -
    -		return rootMetadata, nil
    -
    -	default:
    -		return nil, tuf.ErrUnknownRootMetadataVersion
    -	}
    +	return metadata.GetRootMetadata(false) // never migrate controller metadata
     }
     
     // GetTargetsMetadata returns the deserialized payload of the State's
     // TargetsEnvelope for the specified `roleName`.  The `migrate` parameter
     // determines if the schema must be converted to a newer version.
     func (s *State) GetTargetsMetadata(roleName string, migrate bool) (tuf.TargetsMetadata, error) {
    -	e := s.Metadata.TargetsEnvelope
    -	if roleName != TargetsRoleName {
    -		env, ok := s.Metadata.DelegationEnvelopes[roleName]
    -		if !ok {
    -			return nil, ErrMetadataNotFound
    -		}
    -		e = env
    -	}
    -
    -	if e == nil {
    -		return nil, ErrMetadataNotFound
    -	}
    -
    -	payloadBytes, err := e.DecodeB64Payload()
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	inspectTargetsMetadata := map[string]any{}
    -	if err := json.Unmarshal(payloadBytes, &inspectTargetsMetadata); err != nil {
    -		return nil, fmt.Errorf("unable to unmarshal rule file metadata: %w", err)
    -	}
    -
    -	schemaVersion, hasSchemaVersion := inspectTargetsMetadata["schemaVersion"]
    -	switch {
    -	case !hasSchemaVersion:
    -		// this is tufv01
    -		// Something that's not tufv01 may also lack the schemaVersion field and
    -		// enter this code path. At that point, we're relying on the unmarshal
    -		// to return something that's close to tufv01. We may see strange bugs
    -		// if this happens, but it's also likely someone trying to submit
    -		// incorrect metadata / trigger a version rollback, which we do want to
    -		// be aware of.
    -		targetsMetadata := &tufv01.TargetsMetadata{}
    -		if err := json.Unmarshal(payloadBytes, targetsMetadata); err != nil {
    -			return nil, fmt.Errorf("unable to unmarshal rule file metadata: %w", err)
    -		}
    -
    -		if migrate {
    -			return migrations.MigrateTargetsMetadataV01ToV02(targetsMetadata), nil
    -		}
    -
    -		return targetsMetadata, nil
    -
    -	case schemaVersion == tufv02.TargetsVersion:
    -		targetsMetadata := &tufv02.TargetsMetadata{}
    -		if err := json.Unmarshal(payloadBytes, targetsMetadata); err != nil {
    -			return nil, fmt.Errorf("unable to unmarshal rule file metadata: %w", err)
    -		}
    -
    -		return targetsMetadata, nil
    -
    -	default:
    -		return nil, tuf.ErrUnknownTargetsMetadataVersion
    -	}
    +	return s.Metadata.GetTargetsMetadata(roleName, migrate)
     }
     
     func (s *State) HasTargetsRole(roleName string) bool {
    
  • internal/policy/policy_test.go+87 0 modified
    @@ -112,6 +112,93 @@ func TestLoadState(t *testing.T) {
     		assertStatesEqual(t, state, loadedState)
     	})
     
    +	t.Run("fail loading when first targets is signed by wrong key", func(t *testing.T) {
    +		// We can't use createTestRepository because we want to bypass the
    +		// guardrails in state.Commit and state.Apply.
    +		state := createTestStateWithPolicyTargetsSignedByWrongKey(t)
    +		tempDir := t.TempDir()
    +		repo := gitinterface.CreateTestGitRepository(t, tempDir, false)
    +		state.repository = repo
    +
    +		if err := state.Commit(repo, "Initial state", true, false); err != nil {
    +			t.Fatal(err)
    +		}
    +		policyStagingTip, err := repo.GetReference(PolicyStagingRef)
    +		require.Nil(t, err)
    +
    +		if err := repo.SetReference(PolicyRef, policyStagingTip); err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		if err := rsl.NewReferenceEntry(PolicyRef, policyStagingTip).Commit(repo, false); err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		// Now we have the policy committed bypassing guardrails, where the
    +		// first policy has the wrongly signed metadata. Load and ensure we see
    +		// an error.
    +
    +		entry, err := rsl.GetLatestEntry(repo)
    +		require.Nil(t, err)
    +
    +		loadedState, err := LoadState(t.Context(), repo, entry.(*rsl.ReferenceEntry))
    +		require.Error(t, err)
    +		require.Nil(t, loadedState)
    +	})
    +
    +	t.Run("fail loading when targets is signed by invalid metadata", func(t *testing.T) {
    +		repo, state := createTestRepository(t, createTestStateWithPolicy)
    +
    +		rootEnv := state.Metadata.RootEnvelope
    +
    +		targetsMetadata, err := state.GetTargetsMetadata(TargetsRoleName, false)
    +		require.Nil(t, err)
    +
    +		// Re-sign using wrong key and commit bypassing guardrails to ensure we
    +		// validate when loading state
    +		targetsEnv, err := dsse.CreateEnvelope(targetsMetadata)
    +		require.Nil(t, err)
    +
    +		// The state creator uses the root key for the primary rule file
    +		// Sign with a different key
    +		invalidSigner := setupSSHKeysForSigning(t, targets1KeyBytes, targets1PubKeyBytes)
    +		targetsEnv, err = dsse.SignEnvelope(t.Context(), targetsEnv, invalidSigner)
    +		require.Nil(t, err)
    +
    +		newState := &State{
    +			Metadata: &StateMetadata{
    +				RootEnvelope:    rootEnv,
    +				TargetsEnvelope: targetsEnv,
    +			},
    +		}
    +
    +		if err := newState.preprocess(); err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		if err := newState.Commit(repo, "Updated state", true, false); err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		policyStagingTip, err := repo.GetReference(PolicyStagingRef)
    +		require.Nil(t, err)
    +
    +		if err := repo.SetReference(PolicyRef, policyStagingTip); err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		if err := rsl.NewReferenceEntry(PolicyRef, policyStagingTip).Commit(repo, false); err != nil {
    +			t.Fatal(err)
    +		}
    +
    +		entry, err := rsl.GetLatestEntry(repo)
    +		require.Nil(t, err)
    +
    +		loadedState, err := LoadState(t.Context(), repo, entry.(*rsl.ReferenceEntry))
    +		require.Error(t, err)
    +		require.Nil(t, loadedState)
    +	})
    +
     	t.Run("fail loading while verifying multiple states, bad sig", func(t *testing.T) {
     		repo, state := createTestRepository(t, createTestStateWithPolicy)
     		signer := setupSSHKeysForSigning(t, rootKeyBytes, rootPubKeyBytes)
    
  • internal/policy/verify.go+98 2 modified
    @@ -38,6 +38,7 @@ var (
     	ErrCannotVerifyMergeableForTagRef                    = errors.New("cannot verify mergeable into tag reference")
     	ErrNetworkRepositoryDoesNotDeclareRequiredController = errors.New("network repository does not declare required controller repository")
     	ErrNetworkRepositoryHasStaleControllerMetadata       = errors.New("network repository has not fetched latest controller metadata")
    +	ErrMetadataRollbackDetected                          = errors.New("gittuf policy metadata rollback detected")
     )
     
     // PolicyVerifier implements various gittuf verification workflows.
    @@ -734,6 +735,80 @@ func (v *PolicyVerifier) VerifyRelativeForRef(ctx context.Context, firstEntry, l
     	return nil
     }
     
    +func (s *StateMetadata) VerifyNewStateMetadata(_ context.Context, newStateMetadata *StateMetadata) error {
    +	// Check new state's root version number is >= current state's root version number
    +	currentRootMetadata, err := s.GetRootMetadata(false)
    +	if err != nil {
    +		return err
    +	}
    +	newRootMetadata, err := newStateMetadata.GetRootMetadata(false)
    +	if err != nil {
    +		return err
    +	}
    +	if newRootMetadata.GetVersion() < currentRootMetadata.GetVersion() {
    +		return fmt.Errorf("%w: new policy root version is older than current policy root version, %d < %d", ErrMetadataRollbackDetected, newRootMetadata.GetVersion(), currentRootMetadata.GetVersion())
    +	}
    +
    +	// Check new state's rule files have version numbers >= current state's rule file version numbers
    +
    +	// First off, compare the primary rule file
    +	currentTargetsMetadata, err := s.GetTargetsMetadata(TargetsRoleName, false)
    +	if err != nil {
    +		if errors.Is(err, ErrMetadataNotFound) {
    +			// If the current state doesn't have a primary rule file, we can
    +			// return early as there is no risk of rollback with the rule files
    +			// in the new state.
    +			return nil
    +		}
    +
    +		return err
    +	}
    +	newTargetsMetadata, err := newStateMetadata.GetTargetsMetadata(TargetsRoleName, false)
    +	if err != nil {
    +		// At this point, we know the current state has a primary rule file, so
    +		// if the new state doesn't have one, it's a rollback. This MAY change
    +		// when we support deleting rule files, though maybe we don't allow that
    +		// for primary rule files.
    +		if errors.Is(err, ErrMetadataNotFound) {
    +			return fmt.Errorf("%w: new policy is missing primary rule file", ErrMetadataRollbackDetected)
    +		}
    +
    +		return err
    +	}
    +	if newTargetsMetadata.GetVersion() < currentTargetsMetadata.GetVersion() {
    +		return fmt.Errorf("%w: new policy primary rule file version is older than current policy primary rule file version, %d < %d", ErrMetadataRollbackDetected, newTargetsMetadata.GetVersion(), currentTargetsMetadata.GetVersion())
    +	}
    +
    +	// Then compare all delegated rule files.
    +	// We need to check that the rule files in the new state with the same name
    +	// as a rule file in the current state have greater or equal version numbers.
    +	// The new state may have a rule file added that doesn't exist in the
    +	// current state, but it shouldn't have any removed rule files compared to
    +	// the current state as we don't yet support deleting rule files.
    +	for name := range s.DelegationEnvelopes {
    +		currentDelegatedTargetsMetadata, err := s.GetTargetsMetadata(name, true)
    +		if err != nil {
    +			return err
    +		}
    +		newDelegatedTargetsMetadata, err := newStateMetadata.GetTargetsMetadata(name, true)
    +		if err != nil {
    +			// At this point, we know the current state has this delegated rule
    +			// file, so if the new state doesn't have it, it's a rollback.
    +			// This will change once we support deleting rule files.
    +			if errors.Is(err, ErrMetadataNotFound) {
    +				return fmt.Errorf("%w: new policy is missing delegated rule file '%s'", ErrMetadataRollbackDetected, name)
    +			}
    +
    +			return err
    +		}
    +		if newDelegatedTargetsMetadata.GetVersion() < currentDelegatedTargetsMetadata.GetVersion() {
    +			return fmt.Errorf("%w: new policy delegated rule file '%s' version is older than current policy delegated rule file version, %d < %d", ErrMetadataRollbackDetected, name, newDelegatedTargetsMetadata.GetVersion(), currentDelegatedTargetsMetadata.GetVersion())
    +		}
    +	}
    +
    +	return nil
    +}
    +
     // VerifyNewState ensures that when a new policy is encountered, its root role
     // is signed by keys trusted in the current policy.
     func (s *State) VerifyNewState(ctx context.Context, newPolicy *State) error {
    @@ -742,8 +817,29 @@ func (s *State) VerifyNewState(ctx context.Context, newPolicy *State) error {
     		return err
     	}
     
    -	_, err = rootVerifier.Verify(ctx, gitinterface.ZeroHash, newPolicy.Metadata.RootEnvelope)
    -	return err
    +	if _, err := rootVerifier.Verify(ctx, gitinterface.ZeroHash, newPolicy.Metadata.RootEnvelope); err != nil {
    +		return err
    +	}
    +
    +	// Verify state metadata to protect against rollback attacks
    +	if err := s.Metadata.VerifyNewStateMetadata(ctx, newPolicy.Metadata); err != nil {
    +		return err
    +	}
    +
    +	// Verify controller state metadata to protect against rollback attacks upstream
    +	for name, controllerMetadata := range s.ControllerMetadata {
    +		newControllerMetadata, hasController := newPolicy.ControllerMetadata[name]
    +		if !hasController {
    +			slog.Debug(fmt.Sprintf("new policy does not have metadata for the controller '%s', skipping check for rollback of its metadata...", name))
    +			continue
    +		}
    +
    +		if err := controllerMetadata.VerifyNewStateMetadata(ctx, newControllerMetadata); err != nil {
    +			return fmt.Errorf("controller '%s' metadata failed new state metadata verification: %w", name, err)
    +		}
    +	}
    +
    +	return nil
     }
     
     // verifyEntry is a helper to verify an entry's signature using the specified
    
  • internal/policy/verify_test.go+42 0 modified
    @@ -4002,6 +4002,48 @@ func TestStateVerifyNewState(t *testing.T) {
     		err = currentPolicy.VerifyNewState(testCtx, newPolicy)
     		assert.ErrorIs(t, err, ErrVerifierConditionsUnmet)
     	})
    +
    +	t.Run("rollback attack", func(t *testing.T) {
    +		t.Parallel()
    +
    +		oldPolicy := createTestStateWithOnlyRoot(t)
    +		newPolicy := createTestStateWithPolicy(t)
    +
    +		err := oldPolicy.VerifyNewState(testCtx, newPolicy)
    +		assert.Nil(t, err)
    +
    +		// The reverse should fail
    +		err = newPolicy.VerifyNewState(testCtx, oldPolicy)
    +		assert.ErrorIs(t, err, ErrMetadataRollbackDetected)
    +	})
    +
    +	t.Run("verify new state when transitioning from non numbered to numbered metadata", func(t *testing.T) {
    +		t.Parallel()
    +
    +		oldPolicy := createTestStateWithOnlyRootUnnumbered(t)
    +		newPolicy := createTestStateWithPolicy(t)
    +
    +		err := oldPolicy.VerifyNewState(testCtx, newPolicy)
    +		assert.Nil(t, err)
    +
    +		// The reverse should fail
    +		err = newPolicy.VerifyNewState(testCtx, oldPolicy)
    +		assert.ErrorIs(t, err, ErrMetadataRollbackDetected)
    +	})
    +
    +	t.Run("verify new state when transitioning from non-numbered to numbered metadata with both policy and root", func(t *testing.T) {
    +		t.Parallel()
    +
    +		oldPolicy := createTestStateWithPolicyUnnumbered(t)
    +		newPolicy := createTestStateWithPolicy(t)
    +
    +		err := oldPolicy.VerifyNewState(testCtx, newPolicy)
    +		assert.Nil(t, err)
    +
    +		// The reverse should fail
    +		err = newPolicy.VerifyNewState(testCtx, oldPolicy)
    +		assert.ErrorIs(t, err, ErrMetadataRollbackDetected)
    +	})
     }
     
     func getPropagationDirectivesForNetworkRepository(t *testing.T, rootMetadata tuf.RootMetadata) []tuf.PropagationDirective {
    
  • internal/signerverifier/dsse/dsse_test.go+1 1 modified
    @@ -22,7 +22,7 @@ func TestCreateEnvelope(t *testing.T) {
     	env, err := CreateEnvelope(rootMetadata)
     	assert.Nil(t, err)
     	assert.Equal(t, PayloadType, env.PayloadType)
    -	assert.Equal(t, "eyJ0eXBlIjoicm9vdCIsImV4cGlyZXMiOiIiLCJrZXlzIjpudWxsLCJyb2xlcyI6bnVsbH0=", env.Payload)
    +	assert.Equal(t, "eyJ0eXBlIjoicm9vdCIsImV4cGlyZXMiOiIiLCJ2ZXJzaW9uIjoxLCJrZXlzIjpudWxsLCJyb2xlcyI6bnVsbH0=", env.Payload)
     }
     
     func TestSignEnvelope(t *testing.T) {
    
  • internal/tuf/migrations/migrations.go+6 0 modified
    @@ -24,6 +24,9 @@ func MigrateRootMetadataV01ToV02(rootMetadata *tufv01.RootMetadata) *tufv02.Root
     	// Set same expires
     	newRootMetadata.Expires = rootMetadata.Expires
     
    +	// Set same version number
    +	newRootMetadata.Version = rootMetadata.Version
    +
     	// Set repository location
     	newRootMetadata.RepositoryLocation = rootMetadata.RepositoryLocation
     
    @@ -90,6 +93,9 @@ func MigrateTargetsMetadataV01ToV02(targetsMetadata *tufv01.TargetsMetadata) *tu
     	// Set same expires
     	newTargetsMetadata.Expires = targetsMetadata.Expires
     
    +	// Set same version number
    +	newTargetsMetadata.Version = targetsMetadata.Version
    +
     	// Set delegations
     	newTargetsMetadata.Delegations = &tufv02.Delegations{
     		Principals: map[string]tuf.Principal{},
    
  • internal/tuf/tuf.go+14 4 modified
    @@ -94,8 +94,13 @@ type RootMetadata interface {
     	// unenforced
     	SetExpires(expiry string)
     
    -	// SchemaVersion returns the metadata schema version.
    -	SchemaVersion() string
    +	// GetSchemaVersion returns the metadata schema version.
    +	GetSchemaVersion() string
    +
    +	// GetVersion returns the version number of the metadata.
    +	GetVersion() uint64
    +	// IncrementVersion increments the version number of the metadata by 1.
    +	IncrementVersion()
     
     	// GetRepositoryLocation returns the canonical location of the Git
     	// repository.
    @@ -234,8 +239,13 @@ type TargetsMetadata interface {
     	// unenforced
     	SetExpires(expiry string)
     
    -	// SchemaVersion returns the metadata schema version.
    -	SchemaVersion() string
    +	// GetSchemaVersion returns the metadata schema version.
    +	GetSchemaVersion() string
    +
    +	// GetVersion returns the version number of the metadata.
    +	GetVersion() uint64
    +	// IncrementVersion increments the version number of the metadata by 1.
    +	IncrementVersion()
     
     	// GetPrincipals returns all the principals in the rule file.
     	GetPrincipals() map[string]Principal
    
  • internal/tuf/v01/root.go+15 3 modified
    @@ -22,6 +22,7 @@ const (
     type RootMetadata struct {
     	Type               string                     `json:"type"`
     	Expires            string                     `json:"expires"`
    +	Version            uint64                     `json:"version"`
     	RepositoryLocation string                     `json:"repositoryLocation,omitempty"`
     	Keys               map[string]*Key            `json:"keys"`
     	Roles              map[string]Role            `json:"roles"`
    @@ -35,7 +36,8 @@ type RootMetadata struct {
     // NewRootMetadata returns a new instance of RootMetadata.
     func NewRootMetadata() *RootMetadata {
     	return &RootMetadata{
    -		Type: "root",
    +		Type:    "root",
    +		Version: 1,
     	}
     }
     
    @@ -44,11 +46,21 @@ func (r *RootMetadata) SetExpires(expires string) {
     	r.Expires = expires
     }
     
    -// SchemaVersion returns the metadata schema version.
    -func (r *RootMetadata) SchemaVersion() string {
    +// GetSchemaVersion returns the metadata schema version.
    +func (r *RootMetadata) GetSchemaVersion() string {
     	return rootVersion
     }
     
    +// GetVersion returns the version number of the metadata.
    +func (r *RootMetadata) GetVersion() uint64 {
    +	return r.Version
    +}
    +
    +// IncrementVersion increments the metadata version number by 1.
    +func (r *RootMetadata) IncrementVersion() {
    +	r.Version++
    +}
    +
     // GetRepositoryLocation returns the canonical location of the Git repository.
     func (r *RootMetadata) GetRepositoryLocation() string {
     	return r.RepositoryLocation
    
  • internal/tuf/v01/root_test.go+2 2 modified
    @@ -45,8 +45,8 @@ func TestRootMetadata(t *testing.T) {
     		assert.True(t, rootMetadata.Roles["targets"].KeyIDs.Has(key.KeyID))
     	})
     
    -	t.Run("test SchemaVersion", func(t *testing.T) {
    -		schemaVersion := rootMetadata.SchemaVersion()
    +	t.Run("test GetSchemaVersion", func(t *testing.T) {
    +		schemaVersion := rootMetadata.GetSchemaVersion()
     		assert.Equal(t, rootVersion, schemaVersion)
     	})
     
    
  • internal/tuf/v01/targets.go+14 2 modified
    @@ -21,6 +21,7 @@ const (
     type TargetsMetadata struct {
     	Type        string         `json:"type"`
     	Expires     string         `json:"expires"`
    +	Version     uint64         `json:"version"`
     	Targets     map[string]any `json:"targets"`
     	Delegations *Delegations   `json:"delegations"`
     }
    @@ -29,6 +30,7 @@ type TargetsMetadata struct {
     func NewTargetsMetadata() *TargetsMetadata {
     	return &TargetsMetadata{
     		Type:        "targets",
    +		Version:     1,
     		Delegations: &Delegations{Roles: []*Delegation{AllowRule()}},
     	}
     }
    @@ -39,11 +41,21 @@ func (t *TargetsMetadata) SetExpires(expires string) {
     	t.Expires = expires
     }
     
    -// SchemaVersion returns the metadata schema version.
    -func (t *TargetsMetadata) SchemaVersion() string {
    +// GetSchemaVersion returns the metadata schema version.
    +func (t *TargetsMetadata) GetSchemaVersion() string {
     	return targetsVersion
     }
     
    +// GetVersion returns the version number of the metadata.
    +func (t *TargetsMetadata) GetVersion() uint64 {
    +	return t.Version
    +}
    +
    +// IncrementVersion increments the metadata version number by 1.
    +func (t *TargetsMetadata) IncrementVersion() {
    +	t.Version++
    +}
    +
     // Validate ensures the instance of TargetsMetadata matches gittuf expectations.
     func (t *TargetsMetadata) Validate() error {
     	if len(t.Targets) != 0 {
    
  • internal/tuf/v02/root.go+21 7 modified
    @@ -19,8 +19,9 @@ const (
     // RootMetadata defines the schema of TUF's Root role.
     type RootMetadata struct {
     	Type               string                     `json:"type"`
    -	Version            string                     `json:"schemaVersion"`
    +	SchemaVersion      string                     `json:"schemaVersion"`
     	Expires            string                     `json:"expires"`
    +	Version            uint64                     `json:"version"`
     	RepositoryLocation string                     `json:"repositoryLocation,omitempty"`
     	Principals         map[string]tuf.Principal   `json:"principals"`
     	Roles              map[string]Role            `json:"roles"`
    @@ -34,8 +35,9 @@ type RootMetadata struct {
     // NewRootMetadata returns a new instance of RootMetadata.
     func NewRootMetadata() *RootMetadata {
     	return &RootMetadata{
    -		Type:    "root",
    -		Version: RootVersion,
    +		Type:          "root",
    +		SchemaVersion: RootVersion,
    +		Version:       1,
     	}
     }
     
    @@ -44,11 +46,21 @@ func (r *RootMetadata) SetExpires(expires string) {
     	r.Expires = expires
     }
     
    -// SchemaVersion returns the metadata schema version.
    -func (r *RootMetadata) SchemaVersion() string {
    +// GetSchemaVersion returns the metadata schema version.
    +func (r *RootMetadata) GetSchemaVersion() string {
    +	return r.SchemaVersion
    +}
    +
    +// GetVersion returns the metadata version number.
    +func (r *RootMetadata) GetVersion() uint64 {
     	return r.Version
     }
     
    +// IncrementVersion increments the metadata version number by 1.
    +func (r *RootMetadata) IncrementVersion() {
    +	r.Version++
    +}
    +
     // GetRepositoryLocation returns the canonical location of the Git repository.
     func (r *RootMetadata) GetRepositoryLocation() string {
     	return r.RepositoryLocation
    @@ -352,8 +364,9 @@ func (r *RootMetadata) UnmarshalJSON(data []byte) error {
     	// json.RawMessage in place of tuf interfaces
     	type tempType struct {
     		Type               string                     `json:"type"`
    -		Version            string                     `json:"schemaVersion"`
    +		SchemaVersion      string                     `json:"schemaVersion"`
     		Expires            string                     `json:"expires"`
    +		Version            uint64                     `json:"version"`
     		RepositoryLocation string                     `json:"repositoryLocation,omitempty"`
     		Principals         map[string]json.RawMessage `json:"principals"`
     		Roles              map[string]Role            `json:"roles"`
    @@ -370,8 +383,9 @@ func (r *RootMetadata) UnmarshalJSON(data []byte) error {
     	}
     
     	r.Type = temp.Type
    -	r.Version = temp.Version
    +	r.SchemaVersion = temp.SchemaVersion
     	r.Expires = temp.Expires
    +	r.Version = temp.Version
     	r.RepositoryLocation = temp.RepositoryLocation
     
     	r.Principals = make(map[string]tuf.Principal)
    
  • internal/tuf/v02/root_test.go+2 2 modified
    @@ -53,8 +53,8 @@ func TestRootMetadata(t *testing.T) {
     		assert.True(t, rootMetadata.Roles["targets"].PrincipalIDs.Has(key.KeyID))
     	})
     
    -	t.Run("test SchemaVersion", func(t *testing.T) {
    -		schemaVersion := rootMetadata.SchemaVersion()
    +	t.Run("test GetSchemaVersion", func(t *testing.T) {
    +		schemaVersion := rootMetadata.GetSchemaVersion()
     		assert.Equal(t, RootVersion, schemaVersion)
     	})
     
    
  • internal/tuf/v02/targets.go+22 10 modified
    @@ -22,19 +22,21 @@ var ErrTargetsNotEmpty = errors.New("`targets` field in gittuf Targets metadata
     
     // TargetsMetadata defines the schema of TUF's Targets role.
     type TargetsMetadata struct {
    -	Type        string         `json:"type"`
    -	Version     string         `json:"schemaVersion"`
    -	Expires     string         `json:"expires"`
    -	Targets     map[string]any `json:"targets"`
    -	Delegations *Delegations   `json:"delegations"`
    +	Type          string         `json:"type"`
    +	SchemaVersion string         `json:"schemaVersion"`
    +	Expires       string         `json:"expires"`
    +	Version       uint64         `json:"version"`
    +	Targets       map[string]any `json:"targets"`
    +	Delegations   *Delegations   `json:"delegations"`
     }
     
     // NewTargetsMetadata returns a new instance of TargetsMetadata.
     func NewTargetsMetadata() *TargetsMetadata {
     	return &TargetsMetadata{
    -		Type:        "targets",
    -		Version:     TargetsVersion,
    -		Delegations: &Delegations{Roles: []*Delegation{AllowRule()}},
    +		Type:          "targets",
    +		SchemaVersion: TargetsVersion,
    +		Version:       1,
    +		Delegations:   &Delegations{Roles: []*Delegation{AllowRule()}},
     	}
     }
     
    @@ -44,11 +46,21 @@ func (t *TargetsMetadata) SetExpires(expires string) {
     	t.Expires = expires
     }
     
    -// SchemaVersion returns the metadata schema version.
    -func (t *TargetsMetadata) SchemaVersion() string {
    +// GetSchemaVersion returns the metadata schema version.
    +func (t *TargetsMetadata) GetSchemaVersion() string {
    +	return t.SchemaVersion
    +}
    +
    +// GetVersion returns the metadata version number.
    +func (t *TargetsMetadata) GetVersion() uint64 {
     	return t.Version
     }
     
    +// IncrementVersion increments the metadata version number by 1.
    +func (t *TargetsMetadata) IncrementVersion() {
    +	t.Version++
    +}
    +
     // Validate ensures the instance of TargetsMetadata matches gittuf expectations.
     func (t *TargetsMetadata) Validate() error {
     	if len(t.Targets) != 0 {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.