Skip to content

Commit 4d37f65

Browse files
feat(verification): add discovery client
Signed-off-by: Thomas Fossati <[email protected]>
1 parent 57a35c0 commit 4d37f65

File tree

4 files changed

+332
-7
lines changed

4 files changed

+332
-7
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
module github.com/veraison/apiclient
22

3-
go 1.23.0
3+
go 1.23
44

55
require (
66
github.com/google/uuid v1.3.0
77
github.com/mitchellh/mapstructure v1.5.0
88
github.com/moogar0880/problems v0.1.1
9-
github.com/stretchr/testify v1.9.0
9+
github.com/stretchr/testify v1.10.0
1010
github.com/veraison/cmw v0.2.0
1111
golang.org/x/oauth2 v0.11.0
1212
)
@@ -17,7 +17,7 @@ require (
1717
github.com/golang/protobuf v1.5.3 // indirect
1818
github.com/pmezard/go-difflib v1.0.0 // indirect
1919
github.com/x448/float16 v0.8.4 // indirect
20-
golang.org/x/net v0.14.0 // indirect
20+
golang.org/x/net v0.21.0 // indirect
2121
google.golang.org/appengine v1.6.7 // indirect
2222
google.golang.org/protobuf v1.31.0 // indirect
2323
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ github.com/moogar0880/problems v0.1.1 h1:bktLhq8NDG/czU2ZziYNigBFksx13RaYe5AVdNm
1717
github.com/moogar0880/problems v0.1.1/go.mod h1:5Dxrk2sD7BfBAgnOzQ1yaTiuCYdGPUh49L8Vhfky62c=
1818
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1919
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
21-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
20+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
21+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2222
github.com/veraison/cmw v0.2.0 h1:BWEvwZnD4nn5osq6XwQpTRcGxwV+Su4t6ytdAbVXAJY=
2323
github.com/veraison/cmw v0.2.0/go.mod h1:OiYKk1t6/Fmmg30ZpSMzi4nKr5kt3374sNTkgxC5BDs=
2424
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
2525
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
2626
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
2727
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
28-
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
29-
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
28+
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
29+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
3030
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
3131
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
3232
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

verification/discovery.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2026 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package verification
5+
6+
import (
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"net/http"
11+
"net/url"
12+
13+
"github.com/veraison/apiclient/common"
14+
)
15+
16+
var discoveryMediaType = "application/vnd.veraison.discovery+json"
17+
18+
// DiscoveryConfig holds the configuration for one or more retrievals from the discovery endpoint.
19+
type DiscoveryConfig struct {
20+
caCerts []string // paths to CA certs to be used in addition to system certs for TLS connections
21+
discoveryURI string // URI of the discovery endpoint
22+
client *common.Client // HTTP(s) client connection configuration
23+
useTLS bool // use TLS for server connections
24+
isInsecure bool // allow insecure server connections (only matters when UseTLS is true)
25+
}
26+
27+
type DiscoveryObject struct {
28+
PublicKey json.RawMessage `json:"ear-verification-key,omitempty"`
29+
MediaTypes []string `json:"media-types,omitempty"`
30+
Schemes []string `json:"attestation-schemes,omitempty"`
31+
Version string `json:"version"`
32+
ServiceState string `json:"service-state"`
33+
ApiEndpoints map[string]string `json:"api-endpoints"`
34+
}
35+
36+
// SetDiscoveryURI sets the discovery URI supplied by the user
37+
func (cfg *DiscoveryConfig) SetDiscoveryURI(uri string) error {
38+
u, err := url.Parse(uri)
39+
if err != nil {
40+
return fmt.Errorf("malformed discovery URI: %w", err)
41+
}
42+
if !u.IsAbs() {
43+
return errors.New("the supplied discovery URI is not absolute")
44+
}
45+
cfg.useTLS = u.Scheme == "https"
46+
cfg.discoveryURI = uri
47+
return nil
48+
}
49+
50+
// SetIsInsecure sets the flag to allow insecure server connections
51+
func (cfg *DiscoveryConfig) SetIsInsecure() {
52+
cfg.isInsecure = true
53+
}
54+
55+
// SetCerts sets the CA certificates to the specified paths
56+
func (cfg *DiscoveryConfig) SetCerts(paths []string) error {
57+
if paths == nil || len(paths) == 0 {
58+
return errors.New("no CA certificate paths supplied")
59+
}
60+
cfg.caCerts = paths
61+
return nil
62+
}
63+
64+
// SetClient sets the HTTP(s) client connection configuration
65+
func (cfg *DiscoveryConfig) SetClient(client *common.Client) error {
66+
if client == nil {
67+
return errors.New("no client supplied")
68+
}
69+
cfg.client = client
70+
return nil
71+
}
72+
73+
// Run retrieves the discovery document from the configured endpoint.
74+
// On success, the decoded discovery document is returned.
75+
func (cfg *DiscoveryConfig) Run() (*DiscoveryObject, error) {
76+
if err := cfg.check(); err != nil {
77+
return nil, err
78+
}
79+
80+
// Attach the default client if the user hasn't supplied one
81+
if err := cfg.initClient(); err != nil {
82+
return nil, err
83+
}
84+
85+
res, err := cfg.discovery()
86+
if err != nil {
87+
return nil, fmt.Errorf("discovery failed: %w", err)
88+
}
89+
90+
j := DiscoveryObject{}
91+
92+
err = common.DecodeJSONBody(res, &j)
93+
if err != nil {
94+
return nil, fmt.Errorf("failure JSON decoding response body: %w", err)
95+
}
96+
97+
return &j, nil
98+
}
99+
100+
// discovery creates the GET request to the discovery endpoint and returns the response
101+
func (cfg DiscoveryConfig) discovery() (*http.Response, error) {
102+
req, err := http.NewRequest("GET", cfg.discoveryURI, http.NoBody)
103+
if err != nil {
104+
return nil, fmt.Errorf("building discovery request: %w", err)
105+
}
106+
107+
// add the Accept header
108+
req.Header.Set("Accept", discoveryMediaType)
109+
110+
hc := &cfg.client.HTTPClient
111+
112+
res, err := hc.Do(req)
113+
if err != nil {
114+
return nil, fmt.Errorf("discovery request failed: %w", err)
115+
}
116+
117+
return res, nil
118+
}
119+
120+
// check makes sure that the config object is in good shape
121+
func (cfg DiscoveryConfig) check() error {
122+
if cfg.discoveryURI == "" {
123+
return errors.New("bad configuration: no discovery URI")
124+
}
125+
126+
// It's OK if we don't have a client at this point in time; if needed we
127+
// will instantiate the default one later.
128+
129+
return nil
130+
}
131+
132+
func (cfg *DiscoveryConfig) initClient() error {
133+
if cfg.client != nil {
134+
return nil // client already initialized
135+
}
136+
137+
if !cfg.useTLS {
138+
// Use a nil authenticator.
139+
// The (reasonable) assumption is that the discovery endpoint is always
140+
// unauthenticated.
141+
cfg.client = common.NewClient(nil)
142+
return nil
143+
}
144+
145+
if cfg.isInsecure {
146+
// Ditto about the nil authenticator.
147+
cfg.client = common.NewInsecureTLSClient(nil)
148+
return nil
149+
}
150+
151+
var err error
152+
153+
cfg.client, err = common.NewTLSClient(nil, cfg.caCerts)
154+
155+
return err
156+
}

verification/discovery_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2026 Contributors to the Veraison project.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package verification
5+
6+
import (
7+
"net/http"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"github.com/veraison/apiclient/common"
13+
)
14+
15+
var (
16+
testDiscoveryObject = &DiscoveryObject{
17+
PublicKey: []byte(`{ "alg": "ES256", "crv": "P-256", "kty": "EC", "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" }`),
18+
MediaTypes: []string{
19+
"application/vnd.enacttrust.tpm-evidence",
20+
"application/vnd.parallaxsecond.key-attestation.tpm",
21+
"application/eat+cwt; eat_profile=\"tag:psacertified.org,2023:psa#tfm\"",
22+
"application/vnd.veraison.tsm-report+cbor",
23+
"application/vnd.veraison.configfs-tsm+json",
24+
"application/vnd.parallaxsecond.key-attestation.cca",
25+
"application/psa-attestation-token",
26+
"application/eat-cwt; profile=\"http://arm.com/psa/2.0.0\"",
27+
"application/eat+cwt; eat_profile=\"tag:psacertified.org,2019:psa#legacy\"",
28+
"application/pem-certificate-chain",
29+
"application/eat-collection; profile=\"http://arm.com/CCA-SSD/1.0.0\"",
30+
"application/eat+cwt; eat_profile=\"tag:github.com,2025:veraison/ratsd/cmw\"",
31+
},
32+
Version: "0.0.2511+f1ccf18",
33+
ServiceState: "READY",
34+
ApiEndpoints: map[string]string{
35+
"newChallengeResponseSession": "/challenge-response/v1/newSession",
36+
},
37+
}
38+
39+
testDiscoveryObjectJSON = `{
40+
"ear-verification-key": { "alg": "ES256", "crv": "P-256", "kty": "EC", "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" },
41+
"media-types": [
42+
"application/vnd.enacttrust.tpm-evidence",
43+
"application/vnd.parallaxsecond.key-attestation.tpm",
44+
"application/eat+cwt; eat_profile=\"tag:psacertified.org,2023:psa#tfm\"",
45+
"application/vnd.veraison.tsm-report+cbor",
46+
"application/vnd.veraison.configfs-tsm+json",
47+
"application/vnd.parallaxsecond.key-attestation.cca",
48+
"application/psa-attestation-token",
49+
"application/eat-cwt; profile=\"http://arm.com/psa/2.0.0\"",
50+
"application/eat+cwt; eat_profile=\"tag:psacertified.org,2019:psa#legacy\"",
51+
"application/pem-certificate-chain",
52+
"application/eat-collection; profile=\"http://arm.com/CCA-SSD/1.0.0\"",
53+
"application/eat+cwt; eat_profile=\"tag:github.com,2025:veraison/ratsd/cmw\""
54+
],
55+
"version": "0.0.2511+f1ccf18",
56+
"service-state": "READY",
57+
"api-endpoints": {
58+
"newChallengeResponseSession": "/challenge-response/v1/newSession"
59+
}
60+
}`
61+
testDiscoveryURI = "http://discovery.example.com/.well-known/verification"
62+
testDiscoveryURIHTTPS = "https://discovery.example.com/.well-known/verification"
63+
)
64+
65+
func TestDiscoveryConfig_Run_ok(t *testing.T) {
66+
var err error
67+
68+
expectedBody := testDiscoveryObject
69+
70+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71+
w.Header().Set("Content-Type", discoveryMediaType)
72+
w.WriteHeader(http.StatusOK)
73+
w.Write([]byte(testDiscoveryObjectJSON))
74+
})
75+
76+
client, teardown := common.NewTestingHTTPClient(h)
77+
defer teardown()
78+
79+
var cfg DiscoveryConfig
80+
81+
err = cfg.SetDiscoveryURI(testDiscoveryURI)
82+
require.NoError(t, err)
83+
84+
err = cfg.SetClient(client)
85+
require.NoError(t, err)
86+
87+
actualBody, err := cfg.Run()
88+
assert.NoError(t, err)
89+
assert.Equal(t, expectedBody, actualBody)
90+
}
91+
92+
func TestDiscoveryConfig_Run_fail_bad_object(t *testing.T) {
93+
var err error
94+
95+
badObject := []byte(`[ "deadbeef" ]`)
96+
97+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98+
w.Header().Set("Content-Type", discoveryMediaType)
99+
w.WriteHeader(http.StatusOK)
100+
w.Write(badObject)
101+
})
102+
103+
client, teardown := common.NewTestingHTTPClient(h)
104+
defer teardown()
105+
106+
var cfg DiscoveryConfig
107+
108+
err = cfg.SetDiscoveryURI(testDiscoveryURI)
109+
require.NoError(t, err)
110+
111+
err = cfg.SetClient(client)
112+
require.NoError(t, err)
113+
114+
_, err = cfg.Run()
115+
assert.ErrorContains(t, err, "failure JSON decoding response body")
116+
}
117+
118+
func TestDiscoveryConfig_Run_fail_configuration(t *testing.T) {
119+
var err error
120+
var cfg DiscoveryConfig
121+
122+
_, err = cfg.Run()
123+
assert.ErrorContains(t, err, "bad configuration: no discovery URI")
124+
}
125+
126+
func TestDiscoveryConfig_Run_fail_init_tls_with_nonexistent_certs(t *testing.T) {
127+
var err error
128+
var cfg DiscoveryConfig
129+
130+
err = cfg.SetDiscoveryURI(testDiscoveryURIHTTPS)
131+
require.NoError(t, err)
132+
133+
err = cfg.SetCerts([]string{"/path/to/nonexistent/cert.pem"})
134+
require.NoError(t, err)
135+
136+
_, err = cfg.Run()
137+
assert.ErrorContains(t, err, "could not read cert")
138+
}
139+
140+
func TestDiscoveryConfig_Setters_fail_misc(t *testing.T) {
141+
var err error
142+
var cfg DiscoveryConfig
143+
144+
err = cfg.SetDiscoveryURI("http://[::1]:namedport")
145+
assert.ErrorContains(t, err, "malformed discovery URI")
146+
147+
err = cfg.SetDiscoveryURI("relative/path")
148+
assert.ErrorContains(t, err, "the supplied discovery URI is not absolute")
149+
150+
err = cfg.SetCerts(nil)
151+
assert.ErrorContains(t, err, "no CA certificate paths supplied")
152+
153+
err = cfg.SetClient(nil)
154+
assert.ErrorContains(t, err, "no client supplied")
155+
}
156+
157+
func TestDiscoveryConfig_Setters_ok_misc(t *testing.T) {
158+
var err error
159+
var cfg DiscoveryConfig
160+
161+
err = cfg.SetDiscoveryURI(testDiscoveryURIHTTPS)
162+
assert.NoError(t, err)
163+
164+
err = cfg.SetCerts([]string{"/path/to/cert1.pem", "/path/to/cert2.pem"})
165+
assert.NoError(t, err)
166+
167+
err = cfg.SetClient(common.NewClient(nil))
168+
assert.NoError(t, err)
169+
}

0 commit comments

Comments
 (0)