Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

object/signed/tag: Add signed tag thingy

Runxi Yu 934865f2 d0505e1e

+599
+3
object/signed/tag/doc.go
··· 1 + // Package signedtag extracts tag signing payloads and signatures from raw tag 2 + // object bodies. 3 + package signedtag
+139
object/signed/tag/integration_test.go
··· 1 + package signedtag_test 2 + 3 + import ( 4 + "bytes" 5 + "os" 6 + "os/exec" 7 + "path/filepath" 8 + "testing" 9 + 10 + "codeberg.org/lindenii/furgit/internal/testgit" 11 + objectid "codeberg.org/lindenii/furgit/object/id" 12 + signedtag "codeberg.org/lindenii/furgit/object/signed/tag" 13 + ) 14 + 15 + func setupSSHSignedTag( 16 + t *testing.T, 17 + algo objectid.Algorithm, 18 + ) (payload []byte, allowedSignersPath string, signaturePath string) { 19 + t.Helper() 20 + 21 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) 22 + 23 + signDir := t.TempDir() 24 + 25 + signRoot, err := os.OpenRoot(signDir) 26 + if err != nil { 27 + t.Fatalf("os.OpenRoot(%q): %v", signDir, err) 28 + } 29 + 30 + t.Cleanup(func() { _ = signRoot.Close() }) 31 + 32 + privateKeyPath := filepath.Join(signDir, "signing_key") 33 + allowedSignersPath = filepath.Join(signDir, "allowed_signers") 34 + signaturePath = filepath.Join(signDir, "tag.sig") 35 + 36 + cmd := exec.Command( //nolint:noctx 37 + "ssh-keygen", 38 + "-q", 39 + "-t", "ed25519", 40 + "-N", "", 41 + "-C", "runxiyu@umich.edu", 42 + "-f", privateKeyPath, 43 + ) //#nosec G204 44 + 45 + out, err := cmd.CombinedOutput() 46 + if err != nil { 47 + t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out) 48 + } 49 + 50 + publicKey, err := signRoot.ReadFile("signing_key.pub") 51 + if err != nil { 52 + t.Fatalf("ReadFile(signing_key.pub): %v", err) 53 + } 54 + 55 + err = signRoot.WriteFile( 56 + "allowed_signers", 57 + append([]byte("runxiyu@umich.edu "), publicKey...), 58 + 0o600, 59 + ) 60 + if err != nil { 61 + t.Fatalf("WriteFile(allowed_signers): %v", err) 62 + } 63 + 64 + testRepo.Run(t, "config", "gpg.format", "ssh") 65 + testRepo.Run(t, "config", "user.signingkey", privateKeyPath) 66 + 67 + testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644) 68 + testRepo.Run(t, "add", "file.txt") 69 + testRepo.Run(t, "commit", "-m", "base commit") 70 + testRepo.Run(t, "tag", "-s", "-m", "signed tag", "signed-tag") 71 + 72 + tagID := testRepo.RevParse(t, "signed-tag^{tag}") 73 + body := testRepo.CatFile(t, "tag", tagID) 74 + 75 + tag, err := signedtag.Parse(body, algo) 76 + if err != nil { 77 + t.Fatalf("Parse: %v", err) 78 + } 79 + 80 + signature, ok := tag.AppendSignature(nil, algo) 81 + if !ok { 82 + t.Fatal("missing signature") 83 + } 84 + 85 + err = signRoot.WriteFile("tag.sig", signature, 0o600) 86 + if err != nil { 87 + t.Fatalf("WriteFile(tag.sig): %v", err) 88 + } 89 + 90 + return tag.AppendPayload(nil), allowedSignersPath, signaturePath 91 + } 92 + 93 + func TestSSHSignedTagIntegration(t *testing.T) { 94 + t.Parallel() 95 + 96 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 97 + payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo) 98 + 99 + cmd := exec.Command( //nolint:noctx 100 + "ssh-keygen", 101 + "-Y", "verify", 102 + "-n", "git", 103 + "-f", allowedSignersPath, 104 + "-I", "runxiyu@umich.edu", 105 + "-s", signaturePath, 106 + ) //#nosec G204 107 + cmd.Stdin = bytes.NewReader(payload) 108 + 109 + out, err := cmd.CombinedOutput() 110 + if err != nil { 111 + t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out) 112 + } 113 + }) 114 + } 115 + 116 + func TestSSHSignedTagIntegrationRejectsTamperedPayload(t *testing.T) { 117 + t.Parallel() 118 + 119 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 120 + payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo) 121 + payload = append([]byte(nil), payload...) 122 + payload[len(payload)-2] ^= 1 123 + 124 + cmd := exec.Command( //nolint:noctx 125 + "ssh-keygen", 126 + "-Y", "verify", 127 + "-n", "git", 128 + "-f", allowedSignersPath, 129 + "-I", "runxiyu@umich.edu", 130 + "-s", signaturePath, 131 + ) //#nosec G204 132 + cmd.Stdin = bytes.NewReader(payload) 133 + 134 + out, err := cmd.CombinedOutput() 135 + if err == nil { 136 + t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out) 137 + } 138 + }) 139 + }
+141
object/signed/tag/parse.go
··· 1 + package signedtag 2 + 3 + import ( 4 + "bytes" 5 + "slices" 6 + 7 + objectid "codeberg.org/lindenii/furgit/object/id" 8 + ) 9 + 10 + var signatureBeginLines = [][]byte{ //nolint:gochecknoglobals 11 + []byte("-----BEGIN PGP SIGNATURE-----"), 12 + []byte("-----BEGIN PGP MESSAGE-----"), 13 + []byte("-----BEGIN SSH SIGNATURE-----"), 14 + []byte("-----BEGIN SIGNED MESSAGE-----"), 15 + } 16 + 17 + // Parse parses one raw tag object body for signature extraction. 18 + // 19 + // Git stores the signature for storageAlgo as an in-body ASCII-armored 20 + // trailer, and may store additional signatures for other algorithms in 21 + // gpgsig* headers. 22 + // 23 + // The returned Tag remains valid only while body remains unchanged. 24 + // 25 + // Labels: Deps-Borrowed, Life-Parent. 26 + func Parse(body []byte, storageAlgo objectid.Algorithm) (*Tag, error) { 27 + tag := &Tag{ 28 + body: body, 29 + signatures: make(map[objectid.Algorithm][]byteRange), 30 + } 31 + 32 + signatureStart := len(body) 33 + for i := 0; i < len(body); { 34 + lineStart := i 35 + rel := bytes.IndexByte(body[i:], '\n') 36 + next := len(body) 37 + 38 + lineEnd := len(body) 39 + if rel >= 0 { 40 + lineEnd = i + rel 41 + next = lineEnd + 1 42 + } 43 + 44 + line := body[lineStart:lineEnd] 45 + if slices.ContainsFunc(signatureBeginLines, func(begin []byte) bool { 46 + return bytes.HasPrefix(line, begin) 47 + }) { 48 + signatureStart = lineStart 49 + } 50 + 51 + i = next 52 + } 53 + 54 + payloadStart := 0 55 + payloadEnd := signatureStart 56 + if signatureStart == len(body) { 57 + payloadEnd = len(body) 58 + } 59 + 60 + for i := 0; i < payloadEnd; { 61 + lineStart := i 62 + rel := bytes.IndexByte(body[i:payloadEnd], '\n') 63 + next := payloadEnd 64 + 65 + lineEnd := payloadEnd 66 + if rel >= 0 { 67 + lineEnd = i + rel 68 + next = lineEnd + 1 69 + } 70 + 71 + line := body[lineStart:lineEnd] 72 + i = next 73 + 74 + if len(line) == 0 { 75 + break 76 + } 77 + 78 + if line[0] == ' ' { 79 + continue 80 + } 81 + 82 + key, valueStart, found := bytes.Cut(line, []byte{' '}) 83 + if !found { 84 + continue 85 + } 86 + 87 + algo, ok := objectid.ParseSignatureHeaderName(string(key)) 88 + if !ok { 89 + continue 90 + } 91 + 92 + tag.appendPayloadRange(payloadStart, lineStart) 93 + tag.signatures[algo] = append(tag.signatures[algo], byteRange{ 94 + start: lineEnd - len(valueStart), 95 + end: next, 96 + }) 97 + 98 + for i < payloadEnd { 99 + rel := bytes.IndexByte(body[i:payloadEnd], '\n') 100 + next = payloadEnd 101 + 102 + lineEnd = payloadEnd 103 + if rel >= 0 { 104 + lineEnd = i + rel 105 + next = lineEnd + 1 106 + } 107 + 108 + cont := body[i:lineEnd] 109 + if len(cont) == 0 || cont[0] != ' ' { 110 + break 111 + } 112 + 113 + tag.signatures[algo] = append(tag.signatures[algo], byteRange{ 114 + start: i + 1, 115 + end: next, 116 + }) 117 + 118 + i = next 119 + } 120 + 121 + payloadStart = i 122 + } 123 + 124 + tag.appendPayloadRange(payloadStart, payloadEnd) 125 + if signatureStart != len(body) { 126 + tag.signatures[storageAlgo] = append(tag.signatures[storageAlgo], byteRange{ 127 + start: signatureStart, 128 + end: len(body), 129 + }) 130 + } 131 + 132 + return tag, nil 133 + } 134 + 135 + func (tag *Tag) appendPayloadRange(start, end int) { 136 + if start >= end { 137 + return 138 + } 139 + 140 + tag.payload = append(tag.payload, byteRange{start: start, end: end}) 141 + }
+11
object/signed/tag/payload_append.go
··· 1 + package signedtag 2 + 3 + // AppendPayload appends the tag verification payload to dst, omitting all 4 + // embedded signatures. 5 + func (tag *Tag) AppendPayload(dst []byte) []byte { 6 + for _, part := range tag.payload { 7 + dst = append(dst, tag.body[part.start:part.end]...) 8 + } 9 + 10 + return dst 11 + }
+16
object/signed/tag/signature_algorithms.go
··· 1 + package signedtag 2 + 3 + import objectid "codeberg.org/lindenii/furgit/object/id" 4 + 5 + // Algorithms returns the algorithms for which the tag carries signatures. 6 + func (tag *Tag) Algorithms() []objectid.Algorithm { 7 + var algorithms []objectid.Algorithm 8 + 9 + for _, algo := range objectid.SupportedAlgorithms() { 10 + if _, ok := tag.signatures[algo]; ok { 11 + algorithms = append(algorithms, algo) 12 + } 13 + } 14 + 15 + return algorithms 16 + }
+17
object/signed/tag/signature_append.go
··· 1 + package signedtag 2 + 3 + import objectid "codeberg.org/lindenii/furgit/object/id" 4 + 5 + // AppendSignature appends the signature for algo to dst. 6 + func (tag *Tag) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) { 7 + signature, ok := tag.signatures[algo] 8 + if !ok { 9 + return dst, false 10 + } 11 + 12 + for _, part := range signature { 13 + dst = append(dst, tag.body[part.start:part.end]...) 14 + } 15 + 16 + return dst, true 17 + }
+15
object/signed/tag/tag.go
··· 1 + package signedtag 2 + 3 + import objectid "codeberg.org/lindenii/furgit/object/id" 4 + 5 + // Tag represents the payload and signatures parsed from a raw tag object. 6 + type Tag struct { 7 + body []byte 8 + payload []byteRange 9 + signatures map[objectid.Algorithm][]byteRange 10 + } 11 + 12 + type byteRange struct { 13 + start int 14 + end int 15 + }
+257
object/signed/tag/unit_test.go
··· 1 + package signedtag_test 2 + 3 + import ( 4 + "testing" 5 + 6 + objectid "codeberg.org/lindenii/furgit/object/id" 7 + signedtag "codeberg.org/lindenii/furgit/object/signed/tag" 8 + ) 9 + 10 + func TestParseSignedTag(t *testing.T) { 11 + t.Parallel() 12 + 13 + body := []byte("" + 14 + "object 04b871796dc0420f8e7561a895b52484b701d51a\n" + 15 + "type commit\n" + 16 + "tag signedtag\n" + 17 + "tagger C O Mitter <committer@example.com> 1465981006 +0000\n" + 18 + "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" + 19 + " Version: GnuPG v1\n" + 20 + " \n" + 21 + " header-signature\n" + 22 + " -----END PGP SIGNATURE-----\n" + 23 + "\n" + 24 + "signed tag\n" + 25 + "\n" + 26 + "signed tag message body\n" + 27 + "-----BEGIN PGP SIGNATURE-----\n" + 28 + "Version: GnuPG v1\n" + 29 + "\n" + 30 + "body-signature\n" + 31 + "-----END PGP SIGNATURE-----\n") 32 + 33 + tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) 34 + if err != nil { 35 + t.Fatalf("Parse: %v", err) 36 + } 37 + 38 + gotPayload := string(tag.AppendPayload(nil)) 39 + 40 + wantPayload := "" + 41 + "object 04b871796dc0420f8e7561a895b52484b701d51a\n" + 42 + "type commit\n" + 43 + "tag signedtag\n" + 44 + "tagger C O Mitter <committer@example.com> 1465981006 +0000\n" + 45 + "\n" + 46 + "signed tag\n" + 47 + "\n" + 48 + "signed tag message body\n" 49 + if gotPayload != wantPayload { 50 + t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) 51 + } 52 + 53 + gotAlgorithms := tag.Algorithms() 54 + if len(gotAlgorithms) != 2 || gotAlgorithms[0] != objectid.AlgorithmSHA1 || gotAlgorithms[1] != objectid.AlgorithmSHA256 { 55 + t.Fatalf("algorithms mismatch: got %v", gotAlgorithms) 56 + } 57 + 58 + gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1) 59 + if !ok { 60 + t.Fatal("missing sha1 signature") 61 + } 62 + 63 + wantSignature := "" + 64 + "-----BEGIN PGP SIGNATURE-----\n" + 65 + "Version: GnuPG v1\n" + 66 + "\n" + 67 + "body-signature\n" + 68 + "-----END PGP SIGNATURE-----\n" 69 + if string(gotSignature) != wantSignature { 70 + t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) 71 + } 72 + 73 + gotHeaderSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256) 74 + if !ok { 75 + t.Fatal("missing sha256 signature") 76 + } 77 + 78 + wantHeaderSignature := "" + 79 + "-----BEGIN PGP SIGNATURE-----\n" + 80 + "Version: GnuPG v1\n" + 81 + "\n" + 82 + "header-signature\n" + 83 + "-----END PGP SIGNATURE-----\n" 84 + if string(gotHeaderSignature) != wantHeaderSignature { 85 + t.Fatalf("header signature mismatch:\n got: %q\nwant: %q", string(gotHeaderSignature), wantHeaderSignature) 86 + } 87 + } 88 + 89 + func TestParseHeaderOnlyTagStripsHeaderAndKeepsHeaderSignature(t *testing.T) { 90 + t.Parallel() 91 + 92 + body := []byte("" + 93 + "object deadbeef\n" + 94 + "type commit\n" + 95 + "tag signedtag\n" + 96 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 97 + "gpgsig-sha256 header\n" + 98 + " continued\n" + 99 + "\n" + 100 + "message\n") 101 + 102 + tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) 103 + if err != nil { 104 + t.Fatalf("Parse: %v", err) 105 + } 106 + 107 + gotPayload := string(tag.AppendPayload(nil)) 108 + 109 + wantPayload := "" + 110 + "object deadbeef\n" + 111 + "type commit\n" + 112 + "tag signedtag\n" + 113 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 114 + "\n" + 115 + "message\n" 116 + if gotPayload != wantPayload { 117 + t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) 118 + } 119 + 120 + gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256) 121 + if !ok { 122 + t.Fatal("missing sha256 signature") 123 + } 124 + 125 + wantSignature := "" + 126 + "header\n" + 127 + "continued\n" 128 + if string(gotSignature) != wantSignature { 129 + t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) 130 + } 131 + 132 + if _, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1); ok { 133 + t.Fatal("unexpected sha1 signature") 134 + } 135 + } 136 + 137 + func TestParseKeepsUnknownHeaderSignatureTextInPayload(t *testing.T) { 138 + t.Parallel() 139 + 140 + body := []byte("" + 141 + "object deadbeef\n" + 142 + "type commit\n" + 143 + "tag signedtag\n" + 144 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 145 + "gpgsig-future header\n" + 146 + " continued\n" + 147 + "\n" + 148 + "message line\n" + 149 + "-----BEGIN PGP SIGNATURE-----\n" + 150 + "body-signature\n" + 151 + "-----END PGP SIGNATURE-----\n") 152 + 153 + tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) 154 + if err != nil { 155 + t.Fatalf("Parse: %v", err) 156 + } 157 + 158 + gotPayload := string(tag.AppendPayload(nil)) 159 + 160 + wantPayload := "" + 161 + "object deadbeef\n" + 162 + "type commit\n" + 163 + "tag signedtag\n" + 164 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 165 + "gpgsig-future header\n" + 166 + " continued\n" + 167 + "\n" + 168 + "message line\n" 169 + if gotPayload != wantPayload { 170 + t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) 171 + } 172 + } 173 + 174 + func TestParseKeepsMessageGpgsigTextInPayload(t *testing.T) { 175 + t.Parallel() 176 + 177 + body := []byte("" + 178 + "object deadbeef\n" + 179 + "type commit\n" + 180 + "tag signedtag\n" + 181 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 182 + "\n" + 183 + "message line\n" + 184 + "gpgsig-future header\n" + 185 + " continued\n" + 186 + "-----BEGIN PGP SIGNATURE-----\n" + 187 + "body-signature\n" + 188 + "-----END PGP SIGNATURE-----\n") 189 + 190 + tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) 191 + if err != nil { 192 + t.Fatalf("Parse: %v", err) 193 + } 194 + 195 + gotPayload := string(tag.AppendPayload(nil)) 196 + 197 + wantPayload := "" + 198 + "object deadbeef\n" + 199 + "type commit\n" + 200 + "tag signedtag\n" + 201 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 202 + "\n" + 203 + "message line\n" + 204 + "gpgsig-future header\n" + 205 + " continued\n" 206 + if gotPayload != wantPayload { 207 + t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) 208 + } 209 + } 210 + 211 + func TestParseUsesLastSignatureBeginByPrefix(t *testing.T) { 212 + t.Parallel() 213 + 214 + body := []byte("" + 215 + "object deadbeef\n" + 216 + "type commit\n" + 217 + "tag signedtag\n" + 218 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 219 + "\n" + 220 + "message\n" + 221 + "-----BEGIN PGP SIGNATURE----- stray\n" + 222 + "still message\n" + 223 + "-----BEGIN PGP SIGNATURE----- trailing\n" + 224 + "body-signature\n") 225 + 226 + tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1) 227 + if err != nil { 228 + t.Fatalf("Parse: %v", err) 229 + } 230 + 231 + gotPayload := string(tag.AppendPayload(nil)) 232 + 233 + wantPayload := "" + 234 + "object deadbeef\n" + 235 + "type commit\n" + 236 + "tag signedtag\n" + 237 + "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" + 238 + "\n" + 239 + "message\n" + 240 + "-----BEGIN PGP SIGNATURE----- stray\n" + 241 + "still message\n" 242 + if gotPayload != wantPayload { 243 + t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload) 244 + } 245 + 246 + gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1) 247 + if !ok { 248 + t.Fatal("missing signature") 249 + } 250 + 251 + wantSignature := "" + 252 + "-----BEGIN PGP SIGNATURE----- trailing\n" + 253 + "body-signature\n" 254 + if string(gotSignature) != wantSignature { 255 + t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature) 256 + } 257 + }