2022-12-29 11:24:08 -05:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
2023-01-19 09:57:50 -05:00
/ *
2023-06-02 03:19:23 -04:00
Package fetcher implements a client for the Constellation Resource API .
2023-01-19 09:57:50 -05:00
The fetcher is used to get information from the versions API without having to
authenticate with AWS , where the API is currently hosted . This package should be
used in user - facing application code most of the time , like the CLI .
2023-06-02 03:19:23 -04:00
Each sub - API included in the Constellation Resource API should define it ' s resources by implementing types that implement apiObject .
The new package can then call this package ' s Fetch function to get the resource from the API .
To modify resources , pkg internal / api / client should be used in a similar fashion .
2023-01-19 09:57:50 -05:00
* /
2022-12-29 11:24:08 -05:00
package fetcher
import (
"context"
"encoding/json"
2023-08-25 06:40:47 -04:00
"errors"
2022-12-29 11:24:08 -05:00
"fmt"
"net/http"
2023-09-25 05:53:02 -04:00
"net/url"
2023-08-25 06:40:47 -04:00
"strings"
2023-08-01 10:48:13 -04:00
"github.com/edgelesssys/constellation/v2/internal/sigstore"
2022-12-29 11:24:08 -05:00
)
2023-05-25 12:43:44 -04:00
// NewHTTPClient returns a new http client.
func NewHTTPClient ( ) HTTPClient {
2023-11-23 08:42:13 -05:00
return & http . Client { Transport : & http . Transport {
DisableKeepAlives : true , // DisableKeepAlives fixes concurrency issue see https://stackoverflow.com/a/75816347
Proxy : http . ProxyFromEnvironment ,
} }
2022-12-29 11:24:08 -05:00
}
2023-06-02 03:19:23 -04:00
// Fetch fetches the given apiObject from the public Constellation CDN.
// Fetch does not require authentication.
2023-09-25 05:53:02 -04:00
func Fetch [ T apiObject ] ( ctx context . Context , c HTTPClient , cdnURL string , obj T ) ( T , error ) {
2022-12-29 11:24:08 -05:00
if err := obj . ValidateRequest ( ) ; err != nil {
return * new ( T ) , fmt . Errorf ( "validating request for %T: %w" , obj , err )
}
2023-09-25 05:53:02 -04:00
urlObj , err := url . Parse ( cdnURL )
2022-12-29 11:24:08 -05:00
if err != nil {
2023-09-25 05:53:02 -04:00
return * new ( T ) , fmt . Errorf ( "parsing CDN root URL: %w" , err )
2022-12-29 11:24:08 -05:00
}
2023-09-25 05:53:02 -04:00
urlObj . Path = obj . JSONPath ( )
url := urlObj . String ( )
2022-12-29 11:24:08 -05:00
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , http . NoBody )
if err != nil {
return * new ( T ) , fmt . Errorf ( "creating request for %T: %w" , obj , err )
}
resp , err := c . Do ( req )
if err != nil {
return * new ( T ) , fmt . Errorf ( "sending request for %T: %w" , obj , err )
}
defer resp . Body . Close ( )
switch resp . StatusCode {
case http . StatusOK :
case http . StatusNotFound :
return * new ( T ) , & NotFoundError { fmt . Errorf ( "requesting resource at %s returned status code 404" , url ) }
default :
return * new ( T ) , fmt . Errorf ( "unexpected status code %d while requesting resource" , resp . StatusCode )
}
var newObj T
if err := json . NewDecoder ( resp . Body ) . Decode ( & newObj ) ; err != nil {
return * new ( T ) , fmt . Errorf ( "decoding %T: %w" , obj , err )
}
if newObj . Validate ( ) != nil {
return * new ( T ) , fmt . Errorf ( "received invalid %T: %w" , newObj , newObj . Validate ( ) )
}
return newObj , nil
}
2023-08-01 10:48:13 -04:00
// FetchAndVerify fetches the given apiObject, checks if it can fetch an accompanying signature and verifies if the signature matches the found object.
// The public key used to verify the signature is embedded in the verifier argument.
// FetchAndVerify uses a generic to return a new object of type T.
// Otherwise the caller would have to cast the interface type to a concrete object, which could fail.
2023-09-25 05:53:02 -04:00
func FetchAndVerify [ T apiObject ] ( ctx context . Context , c HTTPClient , cdnURL string , obj T , cosignVerifier sigstore . Verifier ) ( T , error ) {
fetchedObj , err := Fetch ( ctx , c , cdnURL , obj )
2023-08-01 10:48:13 -04:00
if err != nil {
return fetchedObj , fmt . Errorf ( "fetching object: %w" , err )
}
marshalledObj , err := json . Marshal ( fetchedObj )
if err != nil {
return fetchedObj , fmt . Errorf ( "marshalling object: %w" , err )
}
2023-09-25 05:53:02 -04:00
signature , err := Fetch ( ctx , c , cdnURL , signature { Signed : obj . JSONPath ( ) } )
2023-08-01 10:48:13 -04:00
if err != nil {
return fetchedObj , fmt . Errorf ( "fetching signature: %w" , err )
}
err = cosignVerifier . VerifySignature ( marshalledObj , signature . Signature )
if err != nil {
return fetchedObj , fmt . Errorf ( "verifying signature: %w" , err )
}
return fetchedObj , nil
}
2022-12-29 11:24:08 -05:00
// NotFoundError is an error that is returned when a resource is not found.
type NotFoundError struct {
err error
}
func ( e * NotFoundError ) Error ( ) string {
return fmt . Sprintf ( "the requested resource was not found: %s" , e . err . Error ( ) )
}
func ( e * NotFoundError ) Unwrap ( ) error {
return e . err
}
2023-05-25 12:43:44 -04:00
// HTTPClient is an interface for http clients.
type HTTPClient interface {
2022-12-29 11:24:08 -05:00
Do ( req * http . Request ) ( * http . Response , error )
}
2023-06-02 03:19:23 -04:00
type apiObject interface {
ValidateRequest ( ) error
Validate ( ) error
2023-09-25 05:53:02 -04:00
JSONPath ( ) string
2023-06-02 03:19:23 -04:00
}
2023-08-01 10:48:13 -04:00
2023-08-25 06:40:47 -04:00
// signature manages the signature of a object saved at location 'Signed'.
2023-08-01 10:48:13 -04:00
type signature struct {
// Signed is the object that is signed.
2023-09-25 05:53:02 -04:00
Signed string ` json:"-" `
2023-08-01 10:48:13 -04:00
// Signature is the signature of `Signed`.
Signature [ ] byte ` json:"signature" `
}
// URL returns the URL for the request to the config api.
2023-09-25 05:53:02 -04:00
func ( s signature ) JSONPath ( ) string {
return s . Signed + ".sig"
2023-08-01 10:48:13 -04:00
}
// ValidateRequest validates the request.
func ( s signature ) ValidateRequest ( ) error {
2023-08-25 06:40:47 -04:00
if ! strings . HasSuffix ( s . Signed , ".json" ) {
return errors . New ( "signed object missing .json suffix" )
}
return nil
2023-08-01 10:48:13 -04:00
}
2023-08-25 06:40:47 -04:00
// Validate checks that the signature is base64 encoded.
2023-08-01 10:48:13 -04:00
func ( s signature ) Validate ( ) error {
2023-08-25 06:40:47 -04:00
return sigstore . IsBase64 ( s . Signature )
2023-08-01 10:48:13 -04:00
}