Skip to content

Commit ff4b81c

Browse files
committed
Expose single-server default bucket signing in Go SDK
1 parent 8321a99 commit ff4b81c

2 files changed

Lines changed: 119 additions & 6 deletions

File tree

packages/go/edgesign/edgesign.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ var (
2929

3030
// SignInput describes one public edge object URL to sign.
3131
//
32-
// DefaultBucketPath only applies in multi-server mode. When set, SignURL emits
33-
// /{server}/{key} while signing the canonical claims with the real Bucket.
32+
// DefaultBucketPath applies in single-server and multi-server modes. When set,
33+
// SignURL emits /{key} in single-server mode or /{server}/{key} in
34+
// multi-server mode while signing the canonical claims with the real Bucket.
3435
type SignInput struct {
3536
Method string
3637
BaseURL string
@@ -63,9 +64,10 @@ type Claims struct {
6364
// set, the URL must include the same signed range claim. When it is empty, a
6465
// signed range claim remains valid and is returned in Claims.Range.
6566
//
66-
// DefaultBucket only applies in multi-server mode. When set, VerifyURL also
67-
// accepts /{server}/{key} paths and verifies the signature against DefaultBucket
68-
// as the real signed bucket claim.
67+
// DefaultBucket applies in single-server and multi-server modes. When set,
68+
// VerifyURL also accepts /{key} in single-server mode or /{server}/{key} in
69+
// multi-server mode and verifies the signature against DefaultBucket as the real
70+
// signed bucket claim.
6971
type VerifyInput struct {
7072
Method string
7173
URL string
@@ -77,7 +79,8 @@ type VerifyInput struct {
7779
}
7880

7981
// SignURL returns BaseURL/{bucket}/{key}?expires=...&sig=... using the edge
80-
// gateway HMAC canonical form. When Server is set, it returns
82+
// gateway HMAC canonical form unless DefaultBucketPath requests the short
83+
// /{key} form. When Server is set, it returns
8184
// BaseURL/{server}/{bucket}/{key}?expires=...&sig=... using multi-server mode
8285
// unless DefaultBucketPath requests the short /{server}/{key} form.
8386
func SignURL(input SignInput) (string, error) {
@@ -94,6 +97,9 @@ func SignURL(input SignInput) (string, error) {
9497
Secret: input.Secret,
9598
}
9699
if input.Server == "" {
100+
if input.DefaultBucketPath {
101+
return signing.SignURLForModeWithOptions(signingInput, publicpath.ModeSingle, signing.SignOptions{DefaultBucketPath: true})
102+
}
97103
return signing.SignURL(signingInput)
98104
}
99105
raw, err := signing.SignURLForMode(signingInput, publicpath.ModeMulti)
@@ -136,6 +142,13 @@ func VerifyURL(input VerifyInput) (Claims, error) {
136142
func validateURL(input VerifyInput) (signing.Claims, error) {
137143
cfg := signing.ValidationConfig{Secret: input.Secret}
138144
if input.Server == "" {
145+
if input.DefaultBucket != "" {
146+
return signing.ValidateURLForModeWithOptions(input.Method, input.URL, cfg, input.Now, publicpath.ModeSingle, signing.ValidationOptions{
147+
DefaultBucket: func(server string) (string, bool) {
148+
return input.DefaultBucket, server == ""
149+
},
150+
})
151+
}
139152
return signing.ValidateURL(input.Method, input.URL, cfg, input.Now)
140153
}
141154
claims, err := signing.ValidateURLForMode(input.Method, input.URL, cfg, input.Now, publicpath.ModeMulti)

packages/go/edgesign/edgesign_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,106 @@ func TestNoServerUsesLegacySingleServerBehavior(t *testing.T) {
211211
}
212212
}
213213

214+
func TestSingleServerDefaultBucketPathSignAndVerify(t *testing.T) {
215+
now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
216+
expires := now.Add(time.Minute)
217+
raw, err := SignURL(SignInput{
218+
Method: "GET",
219+
BaseURL: "https://files.example.com",
220+
Bucket: "demo",
221+
Key: "file.txt",
222+
Secret: "top-secret",
223+
Expires: expires,
224+
DefaultBucketPath: true,
225+
})
226+
if err != nil {
227+
t.Fatalf("SignURL() error = %v", err)
228+
}
229+
u, err := url.Parse(raw)
230+
if err != nil {
231+
t.Fatalf("parse signed URL: %v", err)
232+
}
233+
if got, want := u.EscapedPath(), "/file.txt"; got != want {
234+
t.Fatalf("path = %q, want %q", got, want)
235+
}
236+
237+
claims, err := VerifyURL(VerifyInput{Method: "GET", URL: raw, Secret: "top-secret", Now: now, DefaultBucket: "demo"})
238+
if err != nil {
239+
t.Fatalf("VerifyURL() error = %v", err)
240+
}
241+
if claims.Server != "" || claims.Bucket != "demo" || claims.Key != "file.txt" {
242+
t.Fatalf("claims = %#v", claims)
243+
}
244+
wantCanonical := "GET\ndemo\nfile.txt\n" + formatUnixSeconds(expires) + "\n\n\n"
245+
if got := CanonicalString(claims); got != wantCanonical {
246+
t.Fatalf("CanonicalString() = %q, want %q", got, wantCanonical)
247+
}
248+
}
249+
250+
func TestSingleServerDefaultBucketPathRequiresDefaultBucket(t *testing.T) {
251+
now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
252+
raw, err := SignURL(SignInput{
253+
Method: "GET",
254+
BaseURL: "https://files.example.com",
255+
Bucket: "demo",
256+
Key: "file.txt",
257+
Secret: "top-secret",
258+
Expires: now.Add(time.Minute),
259+
DefaultBucketPath: true,
260+
})
261+
if err != nil {
262+
t.Fatalf("SignURL() error = %v", err)
263+
}
264+
u, err := url.Parse(raw)
265+
if err != nil {
266+
t.Fatalf("parse signed URL: %v", err)
267+
}
268+
if got, want := u.EscapedPath(), "/file.txt"; got != want {
269+
t.Fatalf("path = %q, want %q", got, want)
270+
}
271+
272+
_, err = VerifyURL(VerifyInput{Method: "GET", URL: raw, Secret: "top-secret", Now: now})
273+
if err == nil {
274+
t.Fatal("VerifyURL() without DefaultBucket succeeded, want error")
275+
}
276+
}
277+
278+
func TestSingleServerDefaultBucketPathShortFormWins(t *testing.T) {
279+
now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
280+
expires := now.Add(time.Minute)
281+
raw, err := SignURL(SignInput{
282+
Method: "GET",
283+
BaseURL: "https://files.example.com",
284+
Bucket: "demo",
285+
Key: "demo/file.txt",
286+
Secret: "top-secret",
287+
Expires: expires,
288+
DefaultBucketPath: true,
289+
})
290+
if err != nil {
291+
t.Fatalf("SignURL() error = %v", err)
292+
}
293+
u, err := url.Parse(raw)
294+
if err != nil {
295+
t.Fatalf("parse signed URL: %v", err)
296+
}
297+
if got, want := u.EscapedPath(), "/demo/file.txt"; got != want {
298+
t.Fatalf("path = %q, want %q", got, want)
299+
}
300+
301+
claims, err := VerifyURL(VerifyInput{Method: "GET", URL: raw, Secret: "top-secret", Now: now, DefaultBucket: "demo"})
302+
if err != nil {
303+
t.Fatalf("VerifyURL() error = %v", err)
304+
}
305+
if claims.Server != "" || claims.Bucket != "demo" || claims.Key != "demo/file.txt" {
306+
t.Fatalf("claims = %#v", claims)
307+
}
308+
wantCanonical := "GET\ndemo\ndemo/file.txt\n" + formatUnixSeconds(expires) + "\n\n\n"
309+
if got := CanonicalString(claims); got != wantCanonical {
310+
t.Fatalf("CanonicalString() = %q, want %q", got, wantCanonical)
311+
}
312+
}
313+
214314
func loadVectors(t *testing.T) []vector {
215315
t.Helper()
216316
data, err := os.ReadFile("../../testdata/edge-signing-vectors.json")

0 commit comments

Comments
 (0)