@@ -22,9 +22,9 @@ import (
2222 "context"
2323 "encoding/json"
2424 "log/slog"
25- "maps"
2625 "net/http"
2726 "slices"
27+ "strings"
2828 "sync"
2929 "time"
3030
@@ -42,133 +42,94 @@ import (
4242 "github.com/gravitational/teleport/lib/sshutils"
4343)
4444
45- type githubMetadataClient interface {
46- fetchETag () (string , error )
47- fetchFingerprints () ([]string , string , error )
45+ // githubServerKeyManager downloads SSH keys from the GitHub meta API and does a
46+ // lazy refresh every hour. The keys are used to verify GitHub server when
47+ // forwarding Git commands to it.
48+ type githubServerKeyManager struct {
49+ mu sync.Mutex
50+ keys []string
51+ lastCheck time.Time
52+ etag string
53+
54+ clock clockwork.Clock
55+ apiEndpoint string
56+ client * http.Client
4857}
4958
50- // githubFingerprintManager downloads SSH fingerprints from the GitHub meta API
51- // and does a lazy refresh every hour. The fingerprints are used to verify
52- // GitHub server when forwarding Git commands to it.
53- type githubFingerprintManager struct {
54- mu sync.RWMutex
55- fingerprints []string
56- lastCheck time.Time
57- etag string
58-
59- clock clockwork.Clock
60- client githubMetadataClient
61- }
62-
63- func newGithubFingerprintManager () * githubFingerprintManager {
64- return & githubFingerprintManager {
65- clock : clockwork .NewRealClock (),
66- client : newGithubMetadataTTPClient (),
59+ func newGitHubServeKeyManager () * githubServerKeyManager {
60+ return & githubServerKeyManager {
61+ clock : clockwork .NewRealClock (),
62+ apiEndpoint : "https://api.github.com/meta" ,
63+ client : & http.Client {
64+ Timeout : defaults .HTTPRequestTimeout ,
65+ },
6766 }
6867}
6968
70- func (g * githubFingerprintManager ) checkServerKey (key ssh.PublicKey ) error {
71- actualFingerprint := ssh .FingerprintSHA256 (key )
72- for _ , fingerprint := range g .getKnownFingerprints () {
73- if sshutils .EqualFingerprints (actualFingerprint , fingerprint ) {
74- return nil
75- }
76- }
77- return trace .BadParameter ("cannot verify github.com: unknown fingerprint %v algo %v" , actualFingerprint , key .Type ())
78- }
69+ func (m * githubServerKeyManager ) check (targetKey ssh.PublicKey ) error {
70+ m .mu .Lock ()
71+ defer m .mu .Unlock ()
7972
80- func (g * githubFingerprintManager ) getKnownFingerprints () []string {
81- const refreshDuration = time .Hour
82- g .mu .RLock ()
83- if g .clock .Now ().Sub (g .lastCheck ) < refreshDuration {
84- defer g .mu .RUnlock ()
85- return g .fingerprints
73+ // Refresh every 24 hours.
74+ if m .clock .Now ().Sub (m .lastCheck ) > time .Hour * 24 {
75+ m .refreshLocked ()
8676 }
87- g .mu .RUnlock ()
8877
89- g . mu . Lock ()
90- defer g . mu . Unlock ( )
91- if g . clock . Now (). Sub ( g . lastCheck ) < refreshDuration {
92- return g . fingerprints
78+ // Remove newline from ssh.MarshalAuthorizedKey.
79+ key := strings . TrimSpace ( string ( ssh . MarshalAuthorizedKey ( targetKey )) )
80+ if slices . Contains ( m . keys , key ) {
81+ return nil
9382 }
83+ return trace .BadParameter ("cannot verify github.com: unknown server key %q" , key )
84+ }
9485
86+ func (m * githubServerKeyManager ) refreshLocked () {
87+ ctx := context .Background ()
9588 logger := slog .With (teleport .ComponentKey , "git:github" )
9689
97- // Check if eTag is the same to avoid downloading the whole thing which
98- // contains a lot of irrelevant info.
99- if g .etag != "" {
100- etag , err := g .client .fetchETag ()
101- switch {
102- case err != nil :
103- logger .WarnContext (context .Background (), "Failed to fetch eTag from GitHub meta API" , "error" , err )
104- // Don't give up yet if HEAD fails.
105-
106- case etag == g .etag :
107- g .lastCheck = g .clock .Now ()
108- logger .DebugContext (context .Background (), "ETag did not change for GitHub meta API" )
109- return g .fingerprints
110-
111- default :
112- logger .DebugContext (context .Background (), "ETag changed for GitHub meta API" , "new" , etag )
113- }
114- }
115-
116- fingerprints , etag , err := g .client .fetchFingerprints ()
90+ // Meta API reference:
91+ // https://docs.github.com/en/rest/meta/meta#get-github-meta-information
92+ req , err := http .NewRequest ("GET" , m .apiEndpoint , nil )
11793 if err != nil {
118- logger .WarnContext (context . Background () , "Failed to fetch fingerprints from GitHub meta API" , "error" , err )
119- return g . fingerprints
94+ logger .WarnContext (ctx , "Failed to make request for GitHub meta API" , "error" , err )
95+ return
12096 }
121- logger .DebugContext (context .Background (), "Found SSH fingerprints from Github meta API" , "fingerprints" , fingerprints , "etag" , etag )
122- g .etag = etag
123- g .fingerprints = fingerprints
124- g .lastCheck = g .clock .Now ()
125- return g .fingerprints
126- }
127-
128- var githubFingerprints = newGithubFingerprintManager ()
129-
130- type githubMetadataHTTPClient struct {
131- api string
132- client * http.Client
133- }
13497
135- func newGithubMetadataTTPClient () * githubMetadataHTTPClient {
136- return & githubMetadataHTTPClient {
137- api : "https://api.github.com/meta" ,
138- client : & http.Client {
139- Timeout : defaults .HTTPRequestTimeout ,
140- },
98+ // ETag check.
99+ if m .etag != "" {
100+ req .Header .Set ("If-None-Match" , m .etag )
141101 }
142- }
143102
144- func (c * githubMetadataHTTPClient ) fetchETag () (string , error ) {
145- resp , err := http .Head (c .api )
103+ resp , err := m .client .Do (req )
146104 if err != nil {
147- return "" , trace .Wrap (err )
105+ logger .WarnContext (ctx , "Failed to fetch GitHub meta API" , "error" , err )
106+ return
148107 }
149- return resp .Header .Get ("ETag" ), nil
150- }
108+ defer resp .Body .Close ()
151109
152- func (c * githubMetadataHTTPClient ) fetchFingerprints () ([]string , string , error ) {
153- resp , err := http .Get (c .api )
154- if err != nil {
155- return nil , "" , trace .Wrap (err )
110+ // Nothing changed. Just update the last check time.
111+ if resp .StatusCode == http .StatusNotModified {
112+ logger .DebugContext (ctx , "GitHub metadata is up-to-date" )
113+ m .lastCheck = m .clock .Now ()
114+ return
156115 }
157- defer resp .Body .Close ()
158116
159- // Meta API reference:
160- // https://docs.github.com/en/rest/meta/meta?apiVersion=2022-11-28#get-github-meta-information
161117 meta := struct {
162- // Fingerprints lists the fingerprints by algo type.
163- Fingerprints map [string ]string `json:"ssh_key_fingerprints"`
118+ SSHKeys []string `json:"ssh_keys"`
164119 }{}
165120 if err := json .NewDecoder (resp .Body ).Decode (& meta ); err != nil {
166- return nil , "" , trace .Wrap (err )
121+ logger .WarnContext (ctx , "Failed to decode response from GitHub meta API" , "error" , err )
122+ return
167123 }
168124
169- return slices .Collect (maps .Values (meta .Fingerprints )), resp .Header .Get ("ETag" ), nil
125+ m .etag = resp .Header .Get ("ETag" )
126+ m .keys = meta .SSHKeys
127+ m .lastCheck = m .clock .Now ()
128+ logger .DebugContext (ctx , "Fetched GitHub metadata" , "ssh_keys" , m .keys , "etag" , m .etag )
170129}
171130
131+ var githubServerKeys = newGitHubServeKeyManager ()
132+
172133// AuthPreferenceGetter is an interface for retrieving the current configured
173134// cluster auth preference.
174135type AuthPreferenceGetter interface {
0 commit comments