diff --git a/crypto/verify.go b/crypto/verify.go new file mode 100644 index 00000000..41458b80 --- /dev/null +++ b/crypto/verify.go @@ -0,0 +1,70 @@ +package crypto + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" + + "github.com/hiddeco/sshsig" + "golang.org/x/crypto/ssh" + "tangled.sh/tangled.sh/core/types" +) + +func VerifySignature(pubKey, signature, payload []byte) (error, bool) { + pub, _, _, _, err := ssh.ParseAuthorizedKey(pubKey) + if err != nil { + return fmt.Errorf("failed to parse public key: %w", err), false + } + + sig, err := sshsig.Unarmor(signature) + if err != nil { + return fmt.Errorf("failed to parse signature: %w", err), false + } + + buf := bytes.NewBuffer(payload) + sshsig.Verify(buf, sig, pub, sshsig.HashSHA256, "git") + return err, err == nil +} + +// VerifyCommitSignature reconstructs the payload used to sign a commit. This is +// essentially the git cat-file output but without the gpgsig header. +// +// Caveats: signature verification will fail on commits with more than one parent, +// i.e. merge commits, because types.NiceDiff doesn't carry more than one Parent field +// and we are unable to reconstruct the payload correctly. +// +// Ideally this should directly operate on an *object.Commit. +func VerifyCommitSignature(pubKey string, commit types.NiceDiff) (error, bool) { + signature := commit.Commit.PGPSignature + + author := bytes.NewBuffer([]byte{}) + committer := bytes.NewBuffer([]byte{}) + commit.Commit.Author.Encode(author) + commit.Commit.Committer.Encode(committer) + + payload := strings.Builder{} + + fmt.Fprintf(&payload, "tree %s\n", commit.Commit.Tree) + fmt.Fprintf(&payload, "parent %s\n", commit.Commit.Parent) + fmt.Fprintf(&payload, "author %s\n", author.String()) + fmt.Fprintf(&payload, "committer %s\n", committer.String()) + if commit.Commit.ChangedId != "" { + fmt.Fprintf(&payload, "change-id %s\n", commit.Commit.ChangedId) + } + fmt.Fprintf(&payload, "\n%s", commit.Commit.Message) + + return VerifySignature([]byte(pubKey), []byte(signature), []byte(payload.String())) +} + +// SSHFingerprint computes the fingerprint of the supplied ssh pubkey. +func SSHFingerprint(pubKey string) (string, error) { + pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) + if err != nil { + return "", err + } + + hash := sha256.Sum256(pk.Marshal()) + return "SHA256:" + base64.StdEncoding.EncodeToString(hash[:]), nil +} diff --git a/go.mod b/go.mod index 633e55cb..223d8e4d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/go-git/go-git/v5 v5.14.0 github.com/google/uuid v1.6.0 github.com/gorilla/sessions v1.4.0 + github.com/hiddeco/sshsig v0.2.0 github.com/ipfs/go-cid v0.5.0 github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/mattn/go-sqlite3 v1.14.24 @@ -31,6 +32,7 @@ require ( github.com/urfave/cli/v3 v3.3.3 github.com/whyrusleeping/cbor-gen v0.3.1 github.com/yuin/goldmark v1.4.13 + golang.org/x/crypto v0.38.0 golang.org/x/net v0.39.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250526154904-3906c5336421 @@ -122,7 +124,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 29cedd3d..d0075ec0 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw= +github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=