Commit fc755889ba

fc755889baf27e51072434797bb4de2076d3f7eb

parent: c2e04ef116

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 02:26 UTC

lfs: tokens carry the key's fingerprint pin beside its id

Closes #303

Layout: unified · split

CHANGELOG.org +6
@@ -300,6 +300,12 @@ missing, =gitbayd admin backup --verify <archive>= names it, and
300 of a public repository made private, loses the token's use with the 300 of a public repository made private, loses the token's use with the
301 access, and an upload token stops working once its repository is 301 access, and an upload token stops working once its repository is
302 archived (#285). 302 archived (#285).
303- LFS transfer tokens carry a hash of their key's fingerprint beside
304 its id, and a token whose key id now belongs to another key is
305 refused, since SQLite gives a new key the id of the last deleted one.
306 The token format changed: tokens minted before the upgrade are
307 refused, and a transfer running across the restart needs running
308 again (#303).
303 309
304* v1.36.0 — 2026-09-23 310* v1.36.0 — 2026-09-23
305 311
internal/httpd/lfs.go +17 −6
@@ -41,7 +41,8 @@ func (s *Server) lfsSecret() ([]byte, error) {
41// for none). A token is bound to the SSH key that obtained it and 41// for none). A token is bound to the SSH key that obtained it and
42// works only while that key is registered, unexpired and on an enabled 42// works only while that key is registered, unexpired and on an enabled
43// account, and while the key still has the access its operation needs 43// account, and while the key still has the access its operation needs
44// on the repo (#285). Without one, public repos allow anonymous 44// on the repo (#285). A key that took over a deleted key's id does not
45// match the token's fingerprint pin (#303). Without one, public repos allow anonymous
45// download only. 46// download only.
46func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { 47func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) {
47 auth := r.Header.Get("Authorization") 48 auth := r.Header.Get("Authorization")
@@ -62,7 +63,7 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) {
62 return "", 0 63 return "", 0
63 } 64 }
64 live, err := s.st.LiveSSHKeys([]int64{g.KeyID}) 65 live, err := s.st.LiveSSHKeys([]int64{g.KeyID})
65 if err != nil || !live[g.KeyID] || !s.lfsKeyAllows(g.KeyID, repo, g.Op == "upload") { 66 if err != nil || !live[g.KeyID] || !s.lfsKeyAllows(g.KeyID, g.KeyPin, repo, g.Op == "upload") {
66 return "", 0 67 return "", 0
67 } 68 }
68 return g.Op, g.KeyID 69 return g.Op, g.KeyID
@@ -75,13 +76,14 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) {
75 76
76// lfsKeyAllows repeats git-lfs-authenticate's access check for the key 77// lfsKeyAllows repeats git-lfs-authenticate's access check for the key
77// now: a deploy key by its binding, any other key by its account's 78// now: a deploy key by its binding, any other key by its account's
78// access narrowed by the key's scope. An archived repo takes no uploads. 79// access narrowed by the key's scope. The key must be the one the token
79func (s *Server) lfsKeyAllows(keyID int64, repo store.Repo, write bool) bool { 80// was minted for, by fingerprint pin. An archived repo takes no uploads.
81func (s *Server) lfsKeyAllows(keyID int64, pin string, repo store.Repo, write bool) bool {
80 if write && repo.Settings.Archived { 82 if write && repo.Settings.Archived {
81 return false 83 return false
82 } 84 }
83 key, err := s.st.SSHKeyByID(keyID) 85 key, err := s.st.SSHKeyByID(keyID)
84 if err != nil { 86 if err != nil || lfs.KeyPin(key.Fingerprint) != pin {
85 return false 87 return false
86 } 88 }
87 if policy.IsDeployScope(key.Scope) { 89 if policy.IsDeployScope(key.Scope) {
@@ -171,7 +173,16 @@ func (s *Server) lfsBatch(w http.ResponseWriter, r *http.Request) {
171 lfsError(w, http.StatusInternalServerError, "lfs secret unavailable") 173 lfsError(w, http.StatusInternalServerError, "lfs secret unavailable")
172 return 174 return
173 } 175 }
174 transferToken := lfs.Sign(secret, repo.ID, keyID, req.Operation, time.Now()) 176 fingerprint := ""
177 if keyID != 0 {
178 key, err := s.st.SSHKeyByID(keyID)
179 if err != nil {
180 lfsError(w, http.StatusNotFound, "repository not found")
181 return
182 }
183 fingerprint = key.Fingerprint
184 }
185 transferToken := lfs.Sign(secret, repo.ID, keyID, fingerprint, req.Operation, time.Now())
175 base := fmt.Sprintf("%s/%s/%s.git/info/lfs/objects", 186 base := fmt.Sprintf("%s/%s/%s.git/info/lfs/objects",
176 strings.TrimSuffix(s.cfg.Server.SiteURL, "/"), repo.OwnerName, repo.Name) 187 strings.TrimSuffix(s.cfg.Server.SiteURL, "/"), repo.OwnerName, repo.Name)
177 authHeader := map[string]string{"Authorization": "Bearer " + transferToken} 188 authHeader := map[string]string{"Authorization": "Bearer " + transferToken}
internal/httpd/lfsauth_test.go +44 −16
@@ -54,14 +54,14 @@ func TestLFSTokenNeedsALiveKey(t *testing.T) {
54 } 54 }
55 55
56 live := addKey("SHA256:live", nil) 56 live := addKey("SHA256:live", nil)
57 tok := lfs.Sign(secret, repo.ID, live, "upload", time.Now()) 57 tok := lfs.Sign(secret, repo.ID, live, "SHA256:live", "upload", time.Now())
58 if op, key := s.lfsAuth(lfsRequest(tok), repo); op != "upload" || key != live { 58 if op, key := s.lfsAuth(lfsRequest(tok), repo); op != "upload" || key != live {
59 t.Fatalf("live key: %q, %d", op, key) 59 t.Fatalf("live key: %q, %d", op, key)
60 } 60 }
61 61
62 past := time.Now().Add(-time.Minute) 62 past := time.Now().Add(-time.Minute)
63 expired := addKey("SHA256:expired", &past) 63 expired := addKey("SHA256:expired", &past)
64 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, expired, "upload", time.Now())), repo); op != "" { 64 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, expired, "SHA256:expired", "upload", time.Now())), repo); op != "" {
65 t.Errorf("expired key: %q", op) 65 t.Errorf("expired key: %q", op)
66 } 66 }
67 67
@@ -73,7 +73,7 @@ func TestLFSTokenNeedsALiveKey(t *testing.T) {
73 } 73 }
74 74
75 other := addKey("SHA256:other", nil) 75 other := addKey("SHA256:other", nil)
76 otherTok := lfs.Sign(secret, repo.ID, other, "download", time.Now()) 76 otherTok := lfs.Sign(secret, repo.ID, other, "SHA256:other", "download", time.Now())
77 if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "download" { 77 if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "download" {
78 t.Fatalf("second key before disable: %q", op) 78 t.Fatalf("second key before disable: %q", op)
79 } 79 }
@@ -97,13 +97,13 @@ func TestLFSAnonymousTokenOnlyDownloadsPublic(t *testing.T) {
97 t.Fatal(err) 97 t.Fatal(err)
98 } 98 }
99 now := time.Now() 99 now := time.Now()
100 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "download", now)), pub); op != "download" { 100 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "", "download", now)), pub); op != "download" {
101 t.Errorf("public download: %q", op) 101 t.Errorf("public download: %q", op)
102 } 102 }
103 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "upload", now)), pub); op != "" { 103 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "", "upload", now)), pub); op != "" {
104 t.Errorf("anonymous upload: %q", op) 104 t.Errorf("anonymous upload: %q", op)
105 } 105 }
106 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, priv.ID, 0, "download", now)), priv); op != "" { 106 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, priv.ID, 0, "", "download", now)), priv); op != "" {
107 t.Errorf("private download: %q", op) 107 t.Errorf("private download: %q", op)
108 } 108 }
109} 109}
@@ -140,7 +140,7 @@ func TestLFSTokenNeedsCurrentAccess(t *testing.T) {
140 t.Fatal(err) 140 t.Fatal(err)
141 } 141 }
142 bobKey := lfsTestKey(t, st, bob, "SHA256:bob", "full") 142 bobKey := lfsTestKey(t, st, bob, "SHA256:bob", "full")
143 up := lfs.Sign(secret, repo.ID, bobKey, "upload", now) 143 up := lfs.Sign(secret, repo.ID, bobKey, "SHA256:bob", "upload", now)
144 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" { 144 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" {
145 t.Fatalf("collaborator upload: %q", op) 145 t.Fatalf("collaborator upload: %q", op)
146 } 146 }
@@ -153,7 +153,7 @@ func TestLFSTokenNeedsCurrentAccess(t *testing.T) {
153 if err := st.RevokeAccess(repo.ID, bob); err != nil { 153 if err := st.RevokeAccess(repo.ID, bob); err != nil {
154 t.Fatal(err) 154 t.Fatal(err)
155 } 155 }
156 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, bobKey, "download", now)), repo); op != "" { 156 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, bobKey, "SHA256:bob", "download", now)), repo); op != "" {
157 t.Errorf("download after access was revoked: %q", op) 157 t.Errorf("download after access was revoked: %q", op)
158 } 158 }
159 159
@@ -163,7 +163,7 @@ func TestLFSTokenNeedsCurrentAccess(t *testing.T) {
163 t.Fatal(err) 163 t.Fatal(err)
164 } 164 }
165 carolKey := lfsTestKey(t, st, carol, "SHA256:carol", "full") 165 carolKey := lfsTestKey(t, st, carol, "SHA256:carol", "full")
166 down := lfs.Sign(secret, pub.ID, carolKey, "download", now) 166 down := lfs.Sign(secret, pub.ID, carolKey, "SHA256:carol", "download", now)
167 if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "download" { 167 if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "download" {
168 t.Fatalf("public download: %q", op) 168 t.Fatalf("public download: %q", op)
169 } 169 }
@@ -194,12 +194,12 @@ func TestLFSDeployKeyToken(t *testing.T) {
194 ro := fmt.Sprintf("deploy:%d:ro", repo.ID) 194 ro := fmt.Sprintf("deploy:%d:ro", repo.ID)
195 195
196 live := lfsTestKey(t, st, u.ID, "SHA256:deploy-live", rw) 196 live := lfsTestKey(t, st, u.ID, "SHA256:deploy-live", rw)
197 if op, key := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, live, "upload", now)), repo); op != "upload" || key != live { 197 if op, key := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, live, "SHA256:deploy-live", "upload", now)), repo); op != "upload" || key != live {
198 t.Fatalf("live deploy key: %q, %d", op, key) 198 t.Fatalf("live deploy key: %q, %d", op, key)
199 } 199 }
200 200
201 removed := lfsTestKey(t, st, u.ID, "SHA256:deploy-removed", rw) 201 removed := lfsTestKey(t, st, u.ID, "SHA256:deploy-removed", rw)
202 tok := lfs.Sign(secret, repo.ID, removed, "download", now) 202 tok := lfs.Sign(secret, repo.ID, removed, "SHA256:deploy-removed", "download", now)
203 if err := st.RemoveDeployKey(repo.ID, "SHA256:deploy-removed"); err != nil { 203 if err := st.RemoveDeployKey(repo.ID, "SHA256:deploy-removed"); err != nil {
204 t.Fatal(err) 204 t.Fatal(err)
205 } 205 }
@@ -208,17 +208,17 @@ func TestLFSDeployKeyToken(t *testing.T) {
208 } 208 }
209 209
210 readOnly := lfsTestKey(t, st, u.ID, "SHA256:deploy-ro", ro) 210 readOnly := lfsTestKey(t, st, u.ID, "SHA256:deploy-ro", ro)
211 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, readOnly, "download", now)), repo); op != "download" { 211 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, readOnly, "SHA256:deploy-ro", "download", now)), repo); op != "download" {
212 t.Errorf("read-only deploy key download: %q", op) 212 t.Errorf("read-only deploy key download: %q", op)
213 } 213 }
214 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, readOnly, "upload", now)), repo); op != "" { 214 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, readOnly, "SHA256:deploy-ro", "upload", now)), repo); op != "" {
215 t.Errorf("read-only deploy key upload: %q", op) 215 t.Errorf("read-only deploy key upload: %q", op)
216 } 216 }
217 217
218 if err := st.SetUserDisabled(u.ID, true); err != nil { 218 if err := st.SetUserDisabled(u.ID, true); err != nil {
219 t.Fatal(err) 219 t.Fatal(err)
220 } 220 }
221 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, live, "download", now)), repo); op != "" { 221 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, live, "SHA256:deploy-live", "download", now)), repo); op != "" {
222 t.Errorf("deploy key of a disabled account: %q", op) 222 t.Errorf("deploy key of a disabled account: %q", op)
223 } 223 }
224} 224}
@@ -234,7 +234,7 @@ func TestLFSUploadTokenRefusedOnceArchived(t *testing.T) {
234 } 234 }
235 key := lfsTestKey(t, st, u.ID, "SHA256:owner", "full") 235 key := lfsTestKey(t, st, u.ID, "SHA256:owner", "full")
236 now := time.Now() 236 now := time.Now()
237 up := lfs.Sign(secret, repo.ID, key, "upload", now) 237 up := lfs.Sign(secret, repo.ID, key, "SHA256:owner", "upload", now)
238 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" { 238 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" {
239 t.Fatalf("upload before archiving: %q", op) 239 t.Fatalf("upload before archiving: %q", op)
240 } 240 }
@@ -248,7 +248,35 @@ func TestLFSUploadTokenRefusedOnceArchived(t *testing.T) {
248 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "" { 248 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "" {
249 t.Errorf("upload after archiving: %q", op) 249 t.Errorf("upload after archiving: %q", op)
250 } 250 }
251 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, key, "download", now)), repo); op != "download" { 251 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, key, "SHA256:owner", "download", now)), repo); op != "download" {
252 t.Errorf("download after archiving: %q", op) 252 t.Errorf("download after archiving: %q", op)
253 } 253 }
254} 254}
255
256// SQLite gives a new key the id of the highest deleted one. A token
257// minted for the deleted key is refused when presented against the new
258// key; the new key's own token works (#303).
259func TestLFSTokenRefusedOnReusedKeyID(t *testing.T) {
260 s, st, u := newTokenTestServer(t)
261 repo := lfsTestRepo(t, st, u.ID, "app", "private")
262 secret, err := s.lfsSecret()
263 if err != nil {
264 t.Fatal(err)
265 }
266 now := time.Now()
267 oldID := lfsTestKey(t, st, u.ID, "SHA256:old", "full")
268 old := lfs.Sign(secret, repo.ID, oldID, "SHA256:old", "upload", now)
269 if err := st.RemoveSSHKey(u.ID, "SHA256:old"); err != nil {
270 t.Fatal(err)
271 }
272 newID := lfsTestKey(t, st, u.ID, "SHA256:new", "full")
273 if newID != oldID {
274 t.Fatalf("new key got id %d, want the reused %d", newID, oldID)
275 }
276 if op, _ := s.lfsAuth(lfsRequest(old), repo); op != "" {
277 t.Errorf("old key's token on the reused id: %q", op)
278 }
279 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, newID, "SHA256:new", "upload", now)), repo); op != "upload" {
280 t.Errorf("new key's own token: %q", op)
281 }
282}
internal/lfs/lfs.go +27 −9
@@ -129,24 +129,39 @@ const TokenTTL = time.Hour
129 129
130// Sign mints a token for op ("download" or "upload") on repoID, bound 130// Sign mints a token for op ("download" or "upload") on repoID, bound
131// to keyID: the SSH key, user or deploy, that asked for it, or 0 for an 131// to keyID: the SSH key, user or deploy, that asked for it, or 0 for an
132// anonymous download of a public repository. 132// anonymous download of a public repository. fingerprint is that key's
133func Sign(secret []byte, repoID, keyID int64, op string, now time.Time) string { 133// fingerprint, "" for key 0. SQLite reuses the id of a deleted key, so
134 payload := fmt.Sprintf("%d:%d:%s:%d", repoID, keyID, op, now.Add(TokenTTL).Unix()) 134// the token carries a hash of the fingerprint as well and a new key
135// given the old id does not inherit the old key's tokens (#303).
136func Sign(secret []byte, repoID, keyID int64, fingerprint, op string, now time.Time) string {
137 payload := fmt.Sprintf("%d:%d:%s:%s:%d", repoID, keyID, KeyPin(fingerprint), op, now.Add(TokenTTL).Unix())
135 mac := hmac.New(sha256.New, secret) 138 mac := hmac.New(sha256.New, secret)
136 mac.Write([]byte(payload)) 139 mac.Write([]byte(payload))
137 return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + 140 return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." +
138 base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) 141 base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
139} 142}
140 143
144// KeyPin is the fingerprint's form in a token: the first 16 hex
145// characters of its SHA-256, or "" for no key.
146func KeyPin(fingerprint string) string {
147 if fingerprint == "" {
148 return ""
149 }
150 sum := sha256.Sum256([]byte(fingerprint))
151 return hex.EncodeToString(sum[:8])
152}
153
141// Grant is what a verified token authorizes. 154// Grant is what a verified token authorizes.
142type Grant struct { 155type Grant struct {
143 RepoID int64 156 RepoID int64
144 KeyID int64 // 0: an anonymous download of a public repository 157 KeyID int64 // 0: an anonymous download of a public repository
158 KeyPin string // KeyPin of the key's fingerprint; "" when KeyID is 0
145 Op string 159 Op string
146} 160}
147 161
148// Verify checks a token's MAC, shape and expiry. A token from before 162// Verify checks a token's MAC, shape and expiry. A token from before
149// tokens named their key does not verify. 163// tokens named their key, or before they carried its fingerprint, does
164// not verify.
150func Verify(secret []byte, token string, now time.Time) (Grant, bool) { 165func Verify(secret []byte, token string, now time.Time) (Grant, bool) {
151 payloadB64, macB64, found := strings.Cut(token, ".") 166 payloadB64, macB64, found := strings.Cut(token, ".")
152 if !found { 167 if !found {
@@ -166,19 +181,22 @@ func Verify(secret []byte, token string, now time.Time) (Grant, bool) {
166 return Grant{}, false 181 return Grant{}, false
167 } 182 }
168 parts := strings.Split(string(payload), ":") 183 parts := strings.Split(string(payload), ":")
169 if len(parts) != 4 { 184 if len(parts) != 5 {
170 return Grant{}, false 185 return Grant{}, false
171 } 186 }
172 repoID, err1 := strconv.ParseInt(parts[0], 10, 64) 187 repoID, err1 := strconv.ParseInt(parts[0], 10, 64)
173 keyID, err2 := strconv.ParseInt(parts[1], 10, 64) 188 keyID, err2 := strconv.ParseInt(parts[1], 10, 64)
174 exp, err3 := strconv.ParseInt(parts[3], 10, 64) 189 exp, err3 := strconv.ParseInt(parts[4], 10, 64)
175 if err1 != nil || err2 != nil || err3 != nil || keyID < 0 || now.Unix() > exp { 190 if err1 != nil || err2 != nil || err3 != nil || keyID < 0 || now.Unix() > exp {
176 return Grant{}, false 191 return Grant{}, false
177 } 192 }
178 if parts[2] != "download" && parts[2] != "upload" { 193 if (keyID == 0) != (parts[2] == "") {
194 return Grant{}, false
195 }
196 if parts[3] != "download" && parts[3] != "upload" {
179 return Grant{}, false 197 return Grant{}, false
180 } 198 }
181 return Grant{RepoID: repoID, KeyID: keyID, Op: parts[2]}, true 199 return Grant{RepoID: repoID, KeyID: keyID, KeyPin: parts[2], Op: parts[3]}, true
182} 200}
183 201
184// NewSecret returns 32 random bytes, hex-encoded for the settings table. 202// NewSecret returns 32 random bytes, hex-encoded for the settings table.
internal/lfs/lfs_test.go +17 −3
@@ -12,9 +12,9 @@ import (
12func TestTokenCarriesTheKey(t *testing.T) { 12func TestTokenCarriesTheKey(t *testing.T) {
13 secret := []byte("secret") 13 secret := []byte("secret")
14 now := time.Now() 14 now := time.Now()
15 tok := Sign(secret, 7, 42, "upload", now) 15 tok := Sign(secret, 7, 42, "SHA256:k", "upload", now)
16 g, ok := Verify(secret, tok, now) 16 g, ok := Verify(secret, tok, now)
17 if !ok || g != (Grant{RepoID: 7, KeyID: 42, Op: "upload"}) { 17 if !ok || g != (Grant{RepoID: 7, KeyID: 42, KeyPin: KeyPin("SHA256:k"), Op: "upload"}) {
18 t.Fatalf("Verify = %+v, %v", g, ok) 18 t.Fatalf("Verify = %+v, %v", g, ok)
19 } 19 }
20 if _, ok := Verify(secret, tok, now.Add(TokenTTL+time.Second)); ok { 20 if _, ok := Verify(secret, tok, now.Add(TokenTTL+time.Second)); ok {
@@ -23,7 +23,7 @@ func TestTokenCarriesTheKey(t *testing.T) {
23 if _, ok := Verify([]byte("other"), tok, now); ok { 23 if _, ok := Verify([]byte("other"), tok, now); ok {
24 t.Error("a token verified under another secret") 24 t.Error("a token verified under another secret")
25 } 25 }
26 if g, ok := Verify(secret, Sign(secret, 7, 0, "download", now), now); !ok || g.KeyID != 0 { 26 if g, ok := Verify(secret, Sign(secret, 7, 0, "", "download", now), now); !ok || g.KeyID != 0 || g.KeyPin != "" {
27 t.Errorf("anonymous grant = %+v, %v", g, ok) 27 t.Errorf("anonymous grant = %+v, %v", g, ok)
28 } 28 }
29} 29}
@@ -41,3 +41,17 @@ func TestUnboundTokenRefused(t *testing.T) {
41 t.Fatalf("a pre-upgrade token verified: %+v", g) 41 t.Fatalf("a pre-upgrade token verified: %+v", g)
42 } 42 }
43} 43}
44
45// A token minted before tokens carried the key's fingerprint has four
46// fields. It is refused (#303).
47func TestUnpinnedTokenRefused(t *testing.T) {
48 secret := []byte("secret")
49 payload := fmt.Sprintf("%d:%d:%s:%d", 7, 42, "upload", time.Now().Add(TokenTTL).Unix())
50 mac := hmac.New(sha256.New, secret)
51 mac.Write([]byte(payload))
52 tok := base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." +
53 base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
54 if g, ok := Verify(secret, tok, time.Now()); ok {
55 t.Fatalf("an unpinned token verified: %+v", g)
56 }
57}
internal/sshd/lfs.go +1 −1
@@ -69,7 +69,7 @@ func runLFSAuthenticate(cfg config.Config, st *store.Store, user store.User, key
69 fmt.Fprintln(stderr, "internal error") 69 fmt.Fprintln(stderr, "internal error")
70 return protocol.ExitFailure 70 return protocol.ExitFailure
71 } 71 }
72 token := lfs.Sign([]byte(secret), repo.ID, key.ID, op, time.Now()) 72 token := lfs.Sign([]byte(secret), repo.ID, key.ID, key.Fingerprint, op, time.Now())
73 json.NewEncoder(stdout).Encode(map[string]any{ 73 json.NewEncoder(stdout).Encode(map[string]any{
74 "href": fmt.Sprintf("%s/%s/%s.git/info/lfs", 74 "href": fmt.Sprintf("%s/%s/%s.git/info/lfs",
75 cfg.Server.SiteURL, repo.OwnerName, repo.Name), 75 cfg.Server.SiteURL, repo.OwnerName, repo.Name),