@@ -22,9 +22,9 @@ import (
22
22
"context"
23
23
"encoding/json"
24
24
"log/slog"
25
- "maps"
26
25
"net/http"
27
26
"slices"
27
+ "strings"
28
28
"sync"
29
29
"time"
30
30
@@ -42,133 +42,94 @@ import (
42
42
"github.com/gravitational/teleport/lib/sshutils"
43
43
)
44
44
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
48
57
}
49
58
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
+ },
67
66
}
68
67
}
69
68
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 ()
79
72
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 ()
86
76
}
87
- g .mu .RUnlock ()
88
77
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
93
82
}
83
+ return trace .BadParameter ("cannot verify github.com: unknown server key %q" , key )
84
+ }
94
85
86
+ func (m * githubServerKeyManager ) refreshLocked () {
87
+ ctx := context .Background ()
95
88
logger := slog .With (teleport .ComponentKey , "git:github" )
96
89
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 )
117
93
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
120
96
}
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
- }
134
97
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 )
141
101
}
142
- }
143
102
144
- func (c * githubMetadataHTTPClient ) fetchETag () (string , error ) {
145
- resp , err := http .Head (c .api )
103
+ resp , err := m .client .Do (req )
146
104
if err != nil {
147
- return "" , trace .Wrap (err )
105
+ logger .WarnContext (ctx , "Failed to fetch GitHub meta API" , "error" , err )
106
+ return
148
107
}
149
- return resp .Header .Get ("ETag" ), nil
150
- }
108
+ defer resp .Body .Close ()
151
109
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
156
115
}
157
- defer resp .Body .Close ()
158
116
159
- // Meta API reference:
160
- // https://docs.github.com/en/rest/meta/meta?apiVersion=2022-11-28#get-github-meta-information
161
117
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"`
164
119
}{}
165
120
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
167
123
}
168
124
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 )
170
129
}
171
130
131
+ var githubServerKeys = newGitHubServeKeyManager ()
132
+
172
133
// AuthPreferenceGetter is an interface for retrieving the current configured
173
134
// cluster auth preference.
174
135
type AuthPreferenceGetter interface {
0 commit comments