Skip to content

Commit 1271fcd

Browse files
feat(api): Add caching control directives to the well-known API handlers
Add the new `discovery-max-age` configuration variable to all API front ends in order to configure the cache control headers for requests to the well-known API. Signed-off-by: Thomas Fossati <thomas.fossati@linaro.org>
1 parent d43bfd1 commit 1271fcd

16 files changed

Lines changed: 195 additions & 59 deletions

File tree

capability/common.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2026 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package capability
4+
5+
import (
6+
"time"
7+
8+
"go.uber.org/zap"
9+
)
10+
11+
// ParseCacheMaxAge parses a "max age" configuration string expressed in time
12+
// units (e.g., "1h", "30m") and returns a time.Duration.
13+
// If the input string is empty, negative or invalid, the function returns the
14+
// provided default duration and logs a warning message.
15+
// A duration string is a possibly signed sequence of decimal numbers, each with
16+
// optional fraction and a unit suffix, such as "300s", "1.5h" or "2h45m".
17+
// Valid time units are "h", "m", "s".
18+
func ParseCacheMaxAge(s string, dflt time.Duration, logger *zap.SugaredLogger) time.Duration {
19+
if s == "" {
20+
return dflt
21+
}
22+
23+
ma, err := time.ParseDuration(s)
24+
if err != nil {
25+
logger.Warnf("invalid .well-known cache max age: %v. resetting to default (%v)", err, dflt)
26+
return dflt
27+
}
28+
29+
if ma < 0 {
30+
logger.Warnf("negative .well-known cache max age: %v. resetting to default (%v)", err, dflt)
31+
return dflt
32+
}
33+
34+
return ma
35+
}

capability/common_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2026 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package capability_test
4+
5+
import (
6+
"testing"
7+
"time"
8+
9+
"github.com/veraison/services/capability"
10+
"go.uber.org/zap"
11+
)
12+
13+
func TestParseCacheMaxAge(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
s string
17+
dflt time.Duration
18+
logger *zap.SugaredLogger
19+
want time.Duration
20+
}{
21+
{
22+
name: "empty string returns default",
23+
s: "",
24+
dflt: 10 * time.Second,
25+
logger: zap.NewNop().Sugar(),
26+
want: 10 * time.Second,
27+
},
28+
{
29+
name: "valid duration string is parsed correctly",
30+
s: "1h30m",
31+
dflt: 10 * time.Second,
32+
logger: zap.NewNop().Sugar(),
33+
want: 90 * time.Minute,
34+
},
35+
{
36+
name: "invalid duration string returns default",
37+
s: "invalid",
38+
dflt: 10 * time.Second,
39+
logger: zap.NewNop().Sugar(),
40+
want: 10 * time.Second,
41+
},
42+
{
43+
name: "negative duration string returns default",
44+
s: "-1h",
45+
dflt: 10 * time.Second,
46+
logger: zap.NewNop().Sugar(),
47+
want: 10 * time.Second,
48+
},
49+
}
50+
for _, tt := range tests {
51+
t.Run(tt.name, func(t *testing.T) {
52+
got := capability.ParseCacheMaxAge(tt.s, tt.dflt, tt.logger)
53+
if got != tt.want {
54+
t.Errorf("ParseCacheMaxAge() = %v, want %v", got, tt.want)
55+
}
56+
})
57+
}
58+
}

coserv/api/handler.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025 Contributors to the Veraison project.
1+
// Copyright 2025-2026 Contributors to the Veraison project.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package api
@@ -15,6 +15,7 @@ import (
1515
"github.com/lestrrat-go/jwx/v2/jwk"
1616
"github.com/veraison/corim/coserv"
1717
"github.com/veraison/go-cose"
18+
"github.com/veraison/services/capability"
1819
"github.com/veraison/services/config"
1920
"github.com/veraison/services/coserv/endorsementdistributor"
2021
"github.com/veraison/services/log"
@@ -28,20 +29,24 @@ var (
2829
"application/coserv+cbor",
2930
"application/coserv+cose",
3031
}
32+
defaultCacheMaxAge = 3600 * time.Second
3133
)
3234

3335
type Handler struct {
3436
Logger *zap.SugaredLogger
3537
EndorsementDistibutor endorsementdistributor.IEndorsementDistributor
38+
WkCacheMaxAge time.Duration
3639
}
3740

3841
func NewHandler(
3942
endorsementdistributor endorsementdistributor.IEndorsementDistributor,
4043
logger *zap.SugaredLogger,
44+
wkCacheMaxAge string,
4145
) Handler {
4246
return Handler{
4347
EndorsementDistibutor: endorsementdistributor,
4448
Logger: logger,
49+
WkCacheMaxAge: capability.ParseCacheMaxAge(wkCacheMaxAge, defaultCacheMaxAge, logger),
4550
}
4651
}
4752

@@ -92,6 +97,8 @@ func (o Handler) GetEdApiWellKnownInfo(c *gin.Context) {
9297
keySet,
9398
)
9499

100+
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", int64(o.WkCacheMaxAge.Seconds())))
101+
c.Header("Expires", time.Now().Add(o.WkCacheMaxAge).UTC().Format(time.RFC1123))
95102
c.Header("Content-Type", CoservDiscoveryMediaType)
96103
c.JSON(http.StatusOK, obj)
97104
}

coserv/api/handler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025 Contributors to the Veraison project.
1+
// Copyright 2026 Contributors to the Veraison project.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package api

coserv/cmd/coserv-service/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ coserv:
77
protocol: https
88
cert: ../../../deployments/docker/src/certs/verification.crt
99
cert-key: ../../../deployments/docker/src/certs/verification.key
10+
discovery-max-age: 1h
1011
vts:
1112
server-addr: localhost:50051
1213
tls: true

coserv/cmd/coserv-service/main.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025 Contributors to the Veraison project.
1+
// Copyright 2025-2026 Contributors to the Veraison project.
22
// SPDX-License-Identifier: Apache-2.0
33
package main
44

@@ -20,17 +20,23 @@ var (
2020
)
2121

2222
type cfg struct {
23-
ListenAddr string `mapstructure:"listen-addr" valid:"dialstring"`
24-
Protocol string `mapstructure:"protocol" valid:"in(http|https)"`
25-
Cert string `mapstructure:"cert" config:"zerodefault"`
26-
CertKey string `mapstructure:"cert-key" config:"zerodefault"`
23+
ListenAddr string `mapstructure:"listen-addr" valid:"dialstring"`
24+
Protocol string `mapstructure:"protocol" valid:"in(http|https)"`
25+
Cert string `mapstructure:"cert" config:"zerodefault"`
26+
CertKey string `mapstructure:"cert-key" config:"zerodefault"`
27+
DiscoveryMaxAge string `mapstructure:"discovery-max-age" config:"zerodefault"`
2728
}
2829

2930
func (o cfg) Validate() error {
3031
if o.Protocol == "https" && (o.Cert == "" || o.CertKey == "") {
3132
return errors.New(`both cert and cert-key must be specified when protocol is "https"`)
3233
}
3334

35+
// Note: we don't validate discovery-max-age here because it is optional and
36+
// has a default value, and the parsing of it is handled in the handler
37+
// where we can log a warning if it's invalid and fall back to the default
38+
// value.
39+
3440
return nil
3541
}
3642

@@ -87,7 +93,7 @@ func main() {
8793
log.Info("initializing endorsement distributor")
8894
endorsementdistributor := endorsementdistributor.New(vtsClient)
8995

90-
apiHandler := api.NewHandler(endorsementdistributor, log.Named("coserv"))
96+
apiHandler := api.NewHandler(endorsementdistributor, log.Named("coserv"), cfg.DiscoveryMaxAge)
9197

9298
if cfg.Protocol == "https" {
9399
apiServerTLS(apiHandler, cfg.ListenAddr, cfg.Cert, cfg.CertKey)

provisioning/api/handler.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022-2024 Contributors to the Veraison project.
1+
// Copyright 2022-2026 Contributors to the Veraison project.
22
// SPDX-License-Identifier: Apache-2.0
33
package api
44

@@ -17,7 +17,8 @@ import (
1717
)
1818

1919
var (
20-
tenantID = "0"
20+
tenantID = "0"
21+
defaultCacheMaxAge = 60 * time.Second
2122
)
2223

2324
type IHandler interface {
@@ -28,16 +29,19 @@ type IHandler interface {
2829
type Handler struct {
2930
Provisioner provisioner.IProvisioner
3031

31-
logger *zap.SugaredLogger
32+
WkCacheMaxAge time.Duration
33+
logger *zap.SugaredLogger
3234
}
3335

3436
func NewHandler(
3537
p provisioner.IProvisioner,
3638
logger *zap.SugaredLogger,
39+
wkCacheMaxAge string,
3740
) IHandler {
3841
return &Handler{
39-
Provisioner: p,
40-
logger: logger,
42+
Provisioner: p,
43+
logger: logger,
44+
WkCacheMaxAge: capability.ParseCacheMaxAge(wkCacheMaxAge, defaultCacheMaxAge, logger),
4145
}
4246
}
4347

@@ -216,6 +220,8 @@ func (o *Handler) GetWellKnownProvisioningInfo(c *gin.Context) {
216220
return
217221
}
218222

223+
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", int64(o.WkCacheMaxAge.Seconds())))
224+
c.Header("Expires", time.Now().Add(o.WkCacheMaxAge).UTC().Format(time.RFC1123))
219225
c.Header("Content-Type", capability.WellKnownMediaType)
220226
c.JSON(http.StatusOK, obj)
221227
}

provisioning/api/handler_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestHandler_Submit_UnsupportedMediaType(t *testing.T) {
7575
SupportedMediaTypes().
7676
Return(supportedMediaTypes, nil)
7777

78-
h := NewHandler(dm, log.Named("test"))
78+
h := NewHandler(dm, log.Named("test"), "1h")
7979

8080
expectedCode := http.StatusUnsupportedMediaType
8181
expectedType := "application/problem+json"
@@ -118,7 +118,7 @@ func TestHandler_Submit_NoBody(t *testing.T) {
118118
).
119119
Return(true, nil)
120120

121-
h := NewHandler(dm, log.Named("test"))
121+
h := NewHandler(dm, log.Named("test"), "1h")
122122

123123
expectedCode := http.StatusBadRequest
124124
expectedType := "application/problem+json"
@@ -168,7 +168,7 @@ func TestHandler_Submit_DecodeFailure(t *testing.T) {
168168
).
169169
Return(errors.New(handlerError))
170170

171-
h := NewHandler(dm, log.Named("test"))
171+
h := NewHandler(dm, log.Named("test"), "1h")
172172

173173
expectedCode := http.StatusOK
174174
expectedType := ProvisioningSessionMediaType
@@ -204,7 +204,7 @@ func TestHandler_Submit_ok(t *testing.T) {
204204
expectedType := ProvisioningSessionMediaType
205205
expectedStatus := "success"
206206
dm := mock_deps.NewMockIProvisioner(ctrl)
207-
h := NewHandler(dm, log.Named("api"))
207+
h := NewHandler(dm, log.Named("api"), "1h")
208208

209209
w := httptest.NewRecorder()
210210
g, _ := gin.CreateTestContext(w)
@@ -249,7 +249,7 @@ func TestHandler_GetWellKnownProvisioningInfo_ok(t *testing.T) {
249249
GetVTSState().
250250
Return(&testGoodServiceState, nil)
251251

252-
h := NewHandler(dm, log.Named("test"))
252+
h := NewHandler(dm, log.Named("test"), "1h")
253253

254254
expectedCode := http.StatusOK
255255
expectedType := capability.WellKnownMediaType
@@ -291,7 +291,7 @@ func TestHandler_GetWellKnownProvisioningInfo_GetRegisteredMediaTypes_empty(t *t
291291
GetVTSState().
292292
Return(&testGoodServiceState, nil)
293293

294-
h := NewHandler(dm, log.Named("test"))
294+
h := NewHandler(dm, log.Named("test"), "1h")
295295

296296
expectedCode := http.StatusOK
297297
expectedType := capability.WellKnownMediaType
@@ -332,7 +332,7 @@ func TestHandler_GetWellKnownProvisioningInfo_GetServiceState_fail(t *testing.T)
332332
GetVTSState().
333333
Return(nil, errors.New("blah"))
334334

335-
h := NewHandler(dm, log.Named("test"))
335+
h := NewHandler(dm, log.Named("test"), "1h")
336336

337337
expectedCode := http.StatusInternalServerError
338338
expectedType := "application/problem+json"

provisioning/cmd/provisioning-service/config-docker.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ provisioning:
77
protocol: https
88
cert: ../../../deployments/docker/src/certs/provisioning.crt
99
cert-key: ../../../deployments/docker/src/certs/provisioning.key
10+
discovery-max-age: 1m
1011
vts:
1112
server-addr: vts-service:50051
1213
tls: true

provisioning/cmd/provisioning-service/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ provisioning:
77
protocol: https
88
cert: ../../../deployments/docker/src/certs/provisioning.crt
99
cert-key: ../../../deployments/docker/src/certs/provisioning.key
10+
discovery-max-age: 1m
1011
vts:
1112
server-addr: localhost:50051
1213
tls: true

0 commit comments

Comments
 (0)