@ -116,7 +116,7 @@ func (r *Repo) AccreditCommit(commit Commit, sigInt sigcred.SignifierInterface)
return commit , fmt . Errorf ( "could not cast commit %+v to interface: %w" , commit , err )
return commit , fmt . Errorf ( "could not cast commit %+v to interface: %w" , commit , err )
}
}
headFS , err := r . H eadFS( )
headFS , err := r . h eadFS( )
if err != nil {
if err != nil {
return commit , fmt . Errorf ( "could not grab snapshot of HEAD fs: %w" , err )
return commit , fmt . Errorf ( "could not grab snapshot of HEAD fs: %w" , err )
}
}
@ -132,22 +132,31 @@ func (r *Repo) AccreditCommit(commit Commit, sigInt sigcred.SignifierInterface)
// Commit uses the given TextMarshaler to create a git commit object (with the
// Commit uses the given TextMarshaler to create a git commit object (with the
// specified accountID as the author) and commits it to the current HEAD,
// specified accountID as the author) and commits it to the current HEAD,
// returning the hash of the commit.
// returning the hash of the commit.
func ( r * Repo ) Commit ( m encoding . TextMarshaler , accountID string ) ( plumbing . Hash , error ) {
func ( r * Repo ) Commit ( m encoding . TextMarshaler , accountID string ) ( GitCommit , error ) {
msgB , err := m . MarshalText ( )
msgB , err := m . MarshalText ( )
if err != nil {
if err != nil {
return plumbing . ZeroHash , fmt . Errorf ( "error marshaling %T to string: %v" , m , err )
return GitCommit { } , fmt . Errorf ( "encoding %T to message string: %v" , m , err )
}
}
w , err := r . GitRepo . Worktree ( )
w , err := r . GitRepo . Worktree ( )
if err != nil {
if err != nil {
return plumbing . ZeroHash , fmt . Errorf ( "could not get git worktree: %w" , err )
return GitCommit { } , fmt . Errorf ( "getting git worktree: %w" , err )
}
}
return w . Commit ( string ( msgB ) , & git . CommitOptions {
h , err := w . Commit ( string ( msgB ) , & git . CommitOptions {
Author : & object . Signature {
Author : & object . Signature {
Name : accountID ,
Name : accountID ,
When : time . Now ( ) ,
When : time . Now ( ) ,
} ,
} ,
} )
} )
if err != nil {
return GitCommit { } , fmt . Errorf ( "committing to git worktree: %w" , err )
}
gc , err := r . GetGitCommit ( h )
if err != nil {
return GitCommit { } , fmt . Errorf ( "retrieving fresh commit %q back from git: %w" , h , err )
}
return gc , nil
}
}
// HasStagedChanges returns true if there are file changes which have been
// HasStagedChanges returns true if there are file changes which have been
@ -176,7 +185,6 @@ func (r *Repo) HasStagedChanges() (bool, error) {
type verificationCtx struct {
type verificationCtx struct {
commit * object . Commit
commit * object . Commit
commitTree , parentTree * object . Tree
commitTree , parentTree * object . Tree
isRootCommit bool
}
}
// non-gophers gonna hate on this method, but I say it's fine
// non-gophers gonna hate on this method, but I say it's fine
@ -188,9 +196,6 @@ func (r *Repo) verificationCtx(h plumbing.Hash) (vctx verificationCtx, err error
return vctx , fmt . Errorf ( "retrieving commit tree object %q: %w" ,
return vctx , fmt . Errorf ( "retrieving commit tree object %q: %w" ,
vctx . commit . TreeHash , err )
vctx . commit . TreeHash , err )
} else if vctx . isRootCommit = vctx . commit . NumParents ( ) == 0 ; vctx . isRootCommit {
vctx . parentTree = new ( object . Tree )
} else if parent , err := vctx . commit . Parent ( 0 ) ; err != nil {
} else if parent , err := vctx . commit . Parent ( 0 ) ; err != nil {
return vctx , fmt . Errorf ( "retrieving commit parent: %w" , err )
return vctx , fmt . Errorf ( "retrieving commit parent: %w" , err )
@ -258,57 +263,84 @@ func (r *Repo) assertAccessControls(
return nil
return nil
}
}
// VerifyCommit verifies that the commit at the given hash, which is presumably
// VerifyCommits verifies that the given commits, which are presumably on the
// on the given branch, is gucci.
// given branch, are gucci.
func ( r * Repo ) VerifyCommit ( branch plumbing . ReferenceName , h plumbing . Hash ) error {
func ( r * Repo ) VerifyCommits ( branch plumbing . ReferenceName , gitCommits [ ] GitCommit ) error {
vctx , err := r . verificationCtx ( h )
if err != nil {
for i , gitCommit := range gitCommits {
return err
// It's not a requirement that the given GitCommits are in ancestral
// order, but usually they are, so we can help verifyCommit not have to
// calculate the parentTree if the previous commit is the parent of this
// one.
var parentTree * object . Tree
if i > 0 && gitCommits [ i - 1 ] . GitCommit . Hash == gitCommit . GitCommit . ParentHashes [ 0 ] {
parentTree = gitCommits [ i - 1 ] . GitTree
}
if err := r . verifyCommit ( branch , gitCommit , parentTree ) ; err != nil {
return fmt . Errorf ( "verifying commit %q: %w" ,
gitCommit . GitCommit . Hash , err )
}
}
return nil
}
// if parentTree is nil then it will be inferred.
func ( r * Repo ) verifyCommit ( branch plumbing . ReferenceName , gitCommit GitCommit , parentTree * object . Tree ) error {
isRoot := gitCommit . Root ( )
if parentTree == nil {
if isRoot {
parentTree = new ( object . Tree )
} else if parentCommit , err := gitCommit . GitCommit . Parent ( 0 ) ; err != nil {
return fmt . Errorf ( "getting parent commit %q: %w" ,
gitCommit . GitCommit . ParentHashes [ 0 ] , err )
} else if parentTree , err = r . GitRepo . TreeObject ( parentCommit . TreeHash ) ; err != nil {
return fmt . Errorf ( "getting parent tree object %q: %w" ,
parentCommit . TreeHash , err )
}
}
vctx := verificationCtx {
commit : gitCommit . GitCommit ,
commitTree : gitCommit . GitTree ,
parentTree : parentTree ,
}
}
var sigFS fs . FS
var sigFS fs . FS
if vctx . isRootCommit {
if isRoot {
sigFS = fs . FromTree ( vctx . commitTree )
sigFS = fs . FromTree ( vctx . commitTree )
} else {
} else {
sigFS = fs . FromTree ( vctx . parentTree )
sigFS = fs . FromTree ( vctx . parentTree )
}
}
var commit Commit
if err := commit . UnmarshalText ( [ ] byte ( vctx . commit . Message ) ) ; err != nil {
return err
}
cfg , err := r . loadConfig ( sigFS )
cfg , err := r . loadConfig ( sigFS )
if err != nil {
if err != nil {
return fmt . Errorf ( "error loading config: %w" , err )
return fmt . Errorf ( "loading config of parent %q: %w" ,
gitCommit . GitCommit . ParentHashes [ 0 ] , err )
}
}
err = r . assertAccessControls ( cfg . AccessControls , commit , vctx , branch )
err = r . assertAccessControls ( cfg . AccessControls , gitCommit . Commit , vctx , branch )
if err != nil {
return fmt . Errorf ( "failed to satisfy all access controls: %w" , err )
}
commitInt , err := commit . Interface ( )
if err != nil {
if err != nil {
return fmt . Errorf ( "could not cast commit %+v to interface: %w" , commit , err )
return fmt . Errorf ( "enforcing access controls: %w" , err )
}
}
changeHash := commitInt . GetHash ( )
changeHash := gitCommit . Interface . GetHash ( )
expectedChangeHash , err := commitInt . Hash ( vctx . parentTree , vctx . commitTree )
expectedChangeHash , err := gitCommit . Interface . Hash ( vctx . parentTree , vctx . commitTree )
if err != nil {
if err != nil {
return fmt . Errorf ( "error calculating expected change hash: %w" , err )
return fmt . Errorf ( "calculating expected change hash: %w" , err )
} else if ! bytes . Equal ( changeHash , expectedChangeHash ) {
} else if ! bytes . Equal ( changeHash , expectedChangeHash ) {
return fmt . Errorf ( "malformed change_hash in commit body, is %s but should be %s" ,
return fmt . Errorf ( "malformed change_hash in commit body, is %s but should be %s" ,
base64 . StdEncoding . EncodeToString ( expectedChangeHash ) ,
base64 . StdEncoding . EncodeToString ( expectedChangeHash ) ,
base64 . StdEncoding . EncodeToString ( changeHash ) )
base64 . StdEncoding . EncodeToString ( changeHash ) )
}
}
for _ , cred := range c ommit. Credentials {
for _ , cred := range gitCommit . C ommit. Credentials {
sig , err := r . signifierForCredential ( sigFS , cred )
sig , err := r . signifierForCredential ( sigFS , cred )
if err != nil {
if err != nil {
return fmt . Errorf ( "error finding signifier for credential %+v: %w" , cred , err )
return fmt . Errorf ( "finding signifier for credential %+v: %w" , cred , err )
} else if err := sig . Verify ( sigFS , expectedChangeHash , cred ) ; err != nil {
} else if err := sig . Verify ( sigFS , expectedChangeHash , cred ) ; err != nil {
return fmt . Errorf ( "error verifying credential %+v: %w" , cred , err )
return fmt . Errorf ( "verifying credential %+v: %w" , cred , err )
}
}
}
}