Commit fc755889ba
Verified · cmc
Layout: unified · split
CHANGELOG.org +6
| @@ -300,6 +300,12 @@ missing, =gitbayd admin backup --verify <archive>= names it, and | ||
| 300 | 300 | of a public repository made private, loses the token's use with the |
| 301 | 301 | access, and an upload token stops working once its repository is |
| 302 | 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 | 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 | 41 | // for none). A token is bound to the SSH key that obtained it and |
| 42 | 42 | // works only while that key is registered, unexpired and on an enabled |
| 43 | 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 | 46 | // download only. |
| 46 | 47 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { |
| 47 | 48 | auth := r.Header.Get("Authorization") |
| @@ -62,7 +63,7 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | ||
| 62 | 63 | return "", 0 |
| 63 | 64 | } |
| 64 | 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 | 67 | return "", 0 |
| 67 | 68 | } |
| 68 | 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 | 77 | // lfsKeyAllows repeats git-lfs-authenticate's access check for the key |
| 77 | 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 | func (s *Server) lfsKeyAllows(keyID int64, repo store.Repo, write bool) bool { | |
| 79 | // access narrowed by the key's scope. The key must be the one the token | |
| 80 | // was minted for, by fingerprint pin. An archived repo takes no uploads. | |
| 81 | func (s *Server) lfsKeyAllows(keyID int64, pin string, repo store.Repo, write bool) bool { | |
| 80 | 82 | if write && repo.Settings.Archived { |
| 81 | 83 | return false |
| 82 | 84 | } |
| 83 | 85 | key, err := s.st.SSHKeyByID(keyID) |
| 84 | if err != nil { | |
| 86 | if err != nil || lfs.KeyPin(key.Fingerprint) != pin { | |
| 85 | 87 | return false |
| 86 | 88 | } |
| 87 | 89 | if policy.IsDeployScope(key.Scope) { |
| @@ -171,7 +173,16 @@ func (s *Server) lfsBatch(w http.ResponseWriter, r *http.Request) { | ||
| 171 | 173 | lfsError(w, http.StatusInternalServerError, "lfs secret unavailable") |
| 172 | 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 | 186 | base := fmt.Sprintf("%s/%s/%s.git/info/lfs/objects", |
| 176 | 187 | strings.TrimSuffix(s.cfg.Server.SiteURL, "/"), repo.OwnerName, repo.Name) |
| 177 | 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 | 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 | 58 | if op, key := s.lfsAuth(lfsRequest(tok), repo); op != "upload" || key != live { |
| 59 | 59 | t.Fatalf("live key: %q, %d", op, key) |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | 62 | past := time.Now().Add(-time.Minute) |
| 63 | 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 | 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 | 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 | 77 | if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "download" { |
| 78 | 78 | t.Fatalf("second key before disable: %q", op) |
| 79 | 79 | } |
| @@ -97,13 +97,13 @@ func TestLFSAnonymousTokenOnlyDownloadsPublic(t *testing.T) { | ||
| 97 | 97 | t.Fatal(err) |
| 98 | 98 | } |
| 99 | 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 | 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 | 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 | 107 | t.Errorf("private download: %q", op) |
| 108 | 108 | } |
| 109 | 109 | } |
| @@ -140,7 +140,7 @@ func TestLFSTokenNeedsCurrentAccess(t *testing.T) { | ||
| 140 | 140 | t.Fatal(err) |
| 141 | 141 | } |
| 142 | 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 | 144 | if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" { |
| 145 | 145 | t.Fatalf("collaborator upload: %q", op) |
| 146 | 146 | } |
| @@ -153,7 +153,7 @@ func TestLFSTokenNeedsCurrentAccess(t *testing.T) { | ||
| 153 | 153 | if err := st.RevokeAccess(repo.ID, bob); err != nil { |
| 154 | 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 | 157 | t.Errorf("download after access was revoked: %q", op) |
| 158 | 158 | } |
| 159 | 159 | |
| @@ -163,7 +163,7 @@ func TestLFSTokenNeedsCurrentAccess(t *testing.T) { | ||
| 163 | 163 | t.Fatal(err) |
| 164 | 164 | } |
| 165 | 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 | 167 | if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "download" { |
| 168 | 168 | t.Fatalf("public download: %q", op) |
| 169 | 169 | } |
| @@ -194,12 +194,12 @@ func TestLFSDeployKeyToken(t *testing.T) { | ||
| 194 | 194 | ro := fmt.Sprintf("deploy:%d:ro", repo.ID) |
| 195 | 195 | |
| 196 | 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 | 198 | t.Fatalf("live deploy key: %q, %d", op, key) |
| 199 | 199 | } |
| 200 | 200 | |
| 201 | 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 | 203 | if err := st.RemoveDeployKey(repo.ID, "SHA256:deploy-removed"); err != nil { |
| 204 | 204 | t.Fatal(err) |
| 205 | 205 | } |
| @@ -208,17 +208,17 @@ func TestLFSDeployKeyToken(t *testing.T) { | ||
| 208 | 208 | } |
| 209 | 209 | |
| 210 | 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 | 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 | 215 | t.Errorf("read-only deploy key upload: %q", op) |
| 216 | 216 | } |
| 217 | 217 | |
| 218 | 218 | if err := st.SetUserDisabled(u.ID, true); err != nil { |
| 219 | 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 | 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 | 235 | key := lfsTestKey(t, st, u.ID, "SHA256:owner", "full") |
| 236 | 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 | 238 | if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" { |
| 239 | 239 | t.Fatalf("upload before archiving: %q", op) |
| 240 | 240 | } |
| @@ -248,7 +248,35 @@ func TestLFSUploadTokenRefusedOnceArchived(t *testing.T) { | ||
| 248 | 248 | if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "" { |
| 249 | 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 | 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). | |
| 259 | func 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 | 130 | // Sign mints a token for op ("download" or "upload") on repoID, bound |
| 131 | 131 | // to keyID: the SSH key, user or deploy, that asked for it, or 0 for an |
| 132 | // anonymous download of a public repository. | |
| 133 | func Sign(secret []byte, repoID, keyID int64, op string, now time.Time) string { | |
| 134 | payload := fmt.Sprintf("%d:%d:%s:%d", repoID, keyID, op, now.Add(TokenTTL).Unix()) | |
| 132 | // anonymous download of a public repository. fingerprint is that key's | |
| 133 | // fingerprint, "" for key 0. SQLite reuses the id of a deleted key, so | |
| 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). | |
| 136 | func 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 | 138 | mac := hmac.New(sha256.New, secret) |
| 136 | 139 | mac.Write([]byte(payload)) |
| 137 | 140 | return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + |
| 138 | 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. | |
| 146 | func 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 | 154 | // Grant is what a verified token authorizes. |
| 142 | 155 | type Grant struct { |
| 143 | 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 | 159 | Op string |
| 146 | 160 | } |
| 147 | 161 | |
| 148 | 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. | |
| 150 | 165 | func Verify(secret []byte, token string, now time.Time) (Grant, bool) { |
| 151 | 166 | payloadB64, macB64, found := strings.Cut(token, ".") |
| 152 | 167 | if !found { |
| @@ -166,19 +181,22 @@ func Verify(secret []byte, token string, now time.Time) (Grant, bool) { | ||
| 166 | 181 | return Grant{}, false |
| 167 | 182 | } |
| 168 | 183 | parts := strings.Split(string(payload), ":") |
| 169 | if len(parts) != 4 { | |
| 184 | if len(parts) != 5 { | |
| 170 | 185 | return Grant{}, false |
| 171 | 186 | } |
| 172 | 187 | repoID, err1 := strconv.ParseInt(parts[0], 10, 64) |
| 173 | 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 | 190 | if err1 != nil || err2 != nil || err3 != nil || keyID < 0 || now.Unix() > exp { |
| 176 | 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 | 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 | 202 | // NewSecret returns 32 random bytes, hex-encoded for the settings table. |
internal/lfs/lfs_test.go +17 −3
| @@ -12,9 +12,9 @@ import ( | ||
| 12 | 12 | func TestTokenCarriesTheKey(t *testing.T) { |
| 13 | 13 | secret := []byte("secret") |
| 14 | 14 | now := time.Now() |
| 15 | tok := Sign(secret, 7, 42, "upload", now) | |
| 15 | tok := Sign(secret, 7, 42, "SHA256:k", "upload", now) | |
| 16 | 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 | 18 | t.Fatalf("Verify = %+v, %v", g, ok) |
| 19 | 19 | } |
| 20 | 20 | if _, ok := Verify(secret, tok, now.Add(TokenTTL+time.Second)); ok { |
| @@ -23,7 +23,7 @@ func TestTokenCarriesTheKey(t *testing.T) { | ||
| 23 | 23 | if _, ok := Verify([]byte("other"), tok, now); ok { |
| 24 | 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 | 27 | t.Errorf("anonymous grant = %+v, %v", g, ok) |
| 28 | 28 | } |
| 29 | 29 | } |
| @@ -41,3 +41,17 @@ func TestUnboundTokenRefused(t *testing.T) { | ||
| 41 | 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). | |
| 47 | func 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 | 69 | fmt.Fprintln(stderr, "internal error") |
| 70 | 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 | 73 | json.NewEncoder(stdout).Encode(map[string]any{ |
| 74 | 74 | "href": fmt.Sprintf("%s/%s/%s.git/info/lfs", |
| 75 | 75 | cfg.Server.SiteURL, repo.OwnerName, repo.Name), |