Skip to content

Commit c92bf8c

Browse files
jshacpu
authored andcommitted
Fix signature generation in Akamai cache purger (#3933)
The EdgeGrid signature scheme signs over the path being requested. When we added the "network" parameter as part of the move to the v3 API, we forgot to include that as part of the path when calculating signatures. This change fixes that and adds a unittest that would have caught it. Part of the unittest changes include changing `akamaiServer` to embed `httptest.Server`. This allows its methods to know what port it's listening on, which is an input to signature checking.
1 parent b76b575 commit c92bf8c

2 files changed

Lines changed: 90 additions & 42 deletions

File tree

akamai/cache-client.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,9 @@ func (cpc *CachePurgeClient) constructAuthHeader(request *http.Request, body []b
145145
header,
146146
)
147147

148-
// Create signing key using a HMAC of the client secret over the timestamp
149-
h := hmac.New(sha256.New, []byte(cpc.clientSecret))
150-
h.Write([]byte(timestamp))
151-
key := make([]byte, base64.StdEncoding.EncodedLen(32))
152-
base64.StdEncoding.Encode(key, h.Sum(nil))
148+
cpc.log.Debugf("To-be-signed Akamai EdgeGrid authentication: %q", tbs)
153149

154-
h = hmac.New(sha256.New, key)
150+
h := hmac.New(sha256.New, signingKey(cpc.clientSecret, timestamp))
155151
h.Write([]byte(tbs))
156152
return fmt.Sprintf(
157153
"%ssignature=%s",
@@ -160,6 +156,16 @@ func (cpc *CachePurgeClient) constructAuthHeader(request *http.Request, body []b
160156
), nil
161157
}
162158

159+
// signingKey makes a signing key by HMAC'ing the timestamp
160+
// using a client secret as the key.
161+
func signingKey(clientSecret string, timestamp string) []byte {
162+
h := hmac.New(sha256.New, []byte(clientSecret))
163+
h.Write([]byte(timestamp))
164+
key := make([]byte, base64.StdEncoding.EncodedLen(32))
165+
base64.StdEncoding.Encode(key, h.Sum(nil))
166+
return key
167+
}
168+
163169
// purge actually sends the individual requests to the Akamai endpoint and checks
164170
// if they are successful
165171
func (cpc *CachePurgeClient) purge(urls []string) error {
@@ -184,7 +190,6 @@ func (cpc *CachePurgeClient) purge(urls []string) error {
184190
if err != nil {
185191
return errFatal(err.Error())
186192
}
187-
188193
req, err := http.NewRequest(
189194
"POST",
190195
endpoint,
@@ -198,7 +203,7 @@ func (cpc *CachePurgeClient) purge(urls []string) error {
198203
authHeader, err := cpc.constructAuthHeader(
199204
req,
200205
reqJSON,
201-
purgePath,
206+
purgePath+cpc.v3Network,
202207
core.RandomString(16),
203208
)
204209
if err != nil {
@@ -207,6 +212,9 @@ func (cpc *CachePurgeClient) purge(urls []string) error {
207212
req.Header.Set("Authorization", authHeader)
208213
req.Header.Set("Content-Type", "application/json")
209214

215+
cpc.log.Debugf("POSTing to %s with Authorization %s: %s",
216+
endpoint, authHeader, reqJSON)
217+
210218
rS := cpc.clk.Now()
211219
resp, err := cpc.client.Do(req)
212220
cpc.stats.TimingDuration("PurgeRequestLatency", time.Since(rS))

akamai/cache-client_test.go

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package akamai
22

33
import (
44
"bytes"
5+
"crypto/hmac"
6+
"crypto/sha256"
7+
"encoding/base64"
58
"encoding/json"
69
"fmt"
710
"io/ioutil"
@@ -18,6 +21,7 @@ import (
1821
)
1922

2023
func TestConstructAuthHeader(t *testing.T) {
24+
log := blog.NewMock()
2125
stats := metrics.NewNoopScope()
2226
cpc, err := NewCachePurgeClient(
2327
"https://akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net",
@@ -27,15 +31,15 @@ func TestConstructAuthHeader(t *testing.T) {
2731
"production",
2832
0,
2933
time.Second,
30-
nil,
34+
log,
3135
stats,
3236
)
3337
test.AssertNotError(t, err, "Failed to create cache purge client")
3438
fc := clock.NewFake()
3539
cpc.clk = fc
3640
wantedTimestamp, err := time.Parse(timestampFormat, "20140321T19:34:21+0000")
3741
test.AssertNotError(t, err, "Failed to parse timestamp")
38-
fc.Add(wantedTimestamp.Sub(fc.Now()))
42+
fc.Set(wantedTimestamp)
3943

4044
req, err := http.NewRequest(
4145
"POST",
@@ -58,6 +62,7 @@ func TestConstructAuthHeader(t *testing.T) {
5862
type akamaiServer struct {
5963
responseCode int
6064
v3 bool
65+
*httptest.Server
6166
}
6267

6368
func (as *akamaiServer) sendResponse(w http.ResponseWriter, resp purgeResponse) {
@@ -95,6 +100,13 @@ func (as *akamaiServer) akamaiHandler(w http.ResponseWriter, r *http.Request) {
95100
return
96101
}
97102

103+
err = as.checkSignature(r, body)
104+
if err != nil {
105+
fmt.Printf("Error checking signature: %s\n", err)
106+
w.WriteHeader(http.StatusInternalServerError)
107+
return
108+
}
109+
98110
// Enforce that a V3 request is well formed and does not include the "Type"
99111
// and "Action" fields used by the V2 api.
100112
if as.v3 == true && (req.Type != "" || req.Action != "") {
@@ -129,19 +141,58 @@ func (as *akamaiServer) akamaiHandler(w http.ResponseWriter, r *http.Request) {
129141
as.sendResponse(w, resp)
130142
}
131143

144+
func (as *akamaiServer) checkSignature(r *http.Request, body []byte) error {
145+
bodyHash := sha256.Sum256(body)
146+
bodyHashB64 := base64.StdEncoding.EncodeToString(bodyHash[:])
147+
148+
authorization := r.Header.Get("Authorization")
149+
authValues := make(map[string]string)
150+
for _, v := range strings.Split(authorization, ";") {
151+
splitValue := strings.Split(v, "=")
152+
authValues[splitValue[0]] = splitValue[1]
153+
}
154+
headerTimestamp := authValues["timestamp"]
155+
splitHeader := strings.Split(authorization, "signature=")
156+
shortenedHeader, signature := splitHeader[0], splitHeader[1]
157+
hostPort := strings.Split(as.URL, "://")[1]
158+
// Assume all unittests use "secret" as the client secret.
159+
h := hmac.New(sha256.New, signingKey("secret", headerTimestamp))
160+
input := []byte(fmt.Sprintf("POST\thttp\t%s\t%s\t\t%s\t%s",
161+
hostPort,
162+
r.URL.Path,
163+
bodyHashB64,
164+
shortenedHeader,
165+
))
166+
h.Write(input)
167+
expectedSignature := base64.StdEncoding.EncodeToString(h.Sum(nil))
168+
if signature != expectedSignature {
169+
return fmt.Errorf("Wrong signature %q in %q. Expected %q\n",
170+
signature, authorization, expectedSignature)
171+
}
172+
return nil
173+
}
174+
175+
func newAkamaiServer(code int, v3 bool) *akamaiServer {
176+
m := http.NewServeMux()
177+
as := akamaiServer{
178+
responseCode: code,
179+
v3: v3,
180+
Server: httptest.NewServer(m),
181+
}
182+
m.HandleFunc("/", as.akamaiHandler)
183+
return &as
184+
}
185+
132186
// TestV2Purge tests the legacy CCU v2 Akamai API used when the v3Network
133187
// parameter to NewCachePurgeClient is "".
134188
func TestV2Purge(t *testing.T) {
135189
log := blog.NewMock()
136190

137-
as := akamaiServer{responseCode: http.StatusCreated}
138-
m := http.NewServeMux()
139-
server := httptest.NewUnstartedServer(m)
140-
m.HandleFunc("/", as.akamaiHandler)
141-
server.Start()
191+
as := newAkamaiServer(http.StatusCreated, false)
192+
defer as.Close()
142193

143194
client, err := NewCachePurgeClient(
144-
server.URL,
195+
as.URL,
145196
"token",
146197
"secret",
147198
"accessToken",
@@ -174,31 +225,24 @@ func TestV2Purge(t *testing.T) {
174225
// TestV3Purge tests the Akamai CCU v3 purge API by setting the v3Network
175226
// parameter to "production".
176227
func TestV3Purge(t *testing.T) {
177-
log := blog.NewMock()
178-
179-
as := akamaiServer{
180-
responseCode: http.StatusCreated,
181-
v3: true,
182-
}
183-
m := http.NewServeMux()
184-
server := httptest.NewUnstartedServer(m)
185-
m.HandleFunc("/", as.akamaiHandler)
186-
server.Start()
228+
as := newAkamaiServer(http.StatusCreated, true)
229+
defer as.Close()
187230

188231
// Client is a purge client with a "production" v3Network parameter
189232
client, err := NewCachePurgeClient(
190-
server.URL,
233+
as.URL,
191234
"token",
192235
"secret",
193236
"accessToken",
194237
"production",
195238
3,
196239
time.Second,
197-
log,
240+
blog.NewMock(),
198241
metrics.NewNoopScope(),
199242
)
200243
test.AssertNotError(t, err, "Failed to create CachePurgeClient")
201-
client.clk = clock.NewFake()
244+
fc := clock.NewFake()
245+
client.clk = fc
202246

203247
err = client.Purge([]string{"http://test.com"})
204248
test.AssertNotError(t, err, "Purge failed with 201 response")
@@ -217,34 +261,30 @@ func TestV3Purge(t *testing.T) {
217261
}
218262

219263
func TestNewCachePurgeClient(t *testing.T) {
220-
log := blog.NewMock()
221-
m := http.NewServeMux()
222-
server := httptest.NewUnstartedServer(m)
223-
224264
// Creating a new cache purge client with an invalid "network" parameter should error
225265
_, err := NewCachePurgeClient(
226-
server.URL,
266+
"http://127.0.0.1:9000/",
227267
"token",
228268
"secret",
229269
"accessToken",
230270
"fake",
231271
3,
232272
time.Second,
233-
log,
273+
blog.NewMock(),
234274
metrics.NewNoopScope(),
235275
)
236276
test.AssertError(t, err, "NewCachePurgeClient with invalid network parameter didn't error")
237277

238278
// Creating a new cache purge client with a valid "network" parameter shouldn't error
239279
_, err = NewCachePurgeClient(
240-
server.URL,
280+
"http://127.0.0.1:9000/",
241281
"token",
242282
"secret",
243283
"accessToken",
244284
"staging",
245285
3,
246286
time.Second,
247-
log,
287+
blog.NewMock(),
248288
metrics.NewNoopScope(),
249289
)
250290
test.AssertNotError(t, err, "NewCachePurgeClient with valid network parameter errored")
@@ -258,7 +298,7 @@ func TestNewCachePurgeClient(t *testing.T) {
258298
"staging",
259299
3,
260300
time.Second,
261-
log,
301+
blog.NewMock(),
262302
metrics.NewNoopScope(),
263303
)
264304
test.AssertError(t, err, "NewCachePurgeClient with invalid server url parameter didn't error")
@@ -267,17 +307,17 @@ func TestNewCachePurgeClient(t *testing.T) {
267307
func TestBigBatchPurge(t *testing.T) {
268308
log := blog.NewMock()
269309

310+
m := http.NewServeMux()
270311
as := akamaiServer{
271312
responseCode: http.StatusCreated,
272313
v3: true,
314+
Server: httptest.NewUnstartedServer(m),
273315
}
274-
m := http.NewServeMux()
275-
server := httptest.NewUnstartedServer(m)
276316
m.HandleFunc("/", as.akamaiHandler)
277-
server.Start()
317+
as.Start()
278318

279319
client, err := NewCachePurgeClient(
280-
server.URL,
320+
as.URL,
281321
"token",
282322
"secret",
283323
"accessToken",

0 commit comments

Comments
 (0)