Files
releases/v1/plugins/rest/auth.go
T
Anders Eknert afb30d3f9d Add gocritic linter, fix a bunch of stuff (#7377)
Brace yourselves! For there are many touched files here. No changes
in semantics however.

Spent a long time trying out the various optional rules gocritic
provides, and settled for a few of them. There are more I really
like, but that would take many hours to address across the codebase.

Perhaps others find gocritic too pedantic? If so, we can merge the
fixes without enabling the rule.

Signed-off-by: Anders Eknert <anders@styra.com>
2025-02-24 16:28:41 +01:00

1010 lines
29 KiB
Go

// Copyright 2019 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package rest
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"hash"
"io"
"math/big"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/open-policy-agent/opa/internal/jwx/jwa"
"github.com/open-policy-agent/opa/internal/jwx/jws"
"github.com/open-policy-agent/opa/internal/jwx/jws/sign"
"github.com/open-policy-agent/opa/internal/providers/aws"
"github.com/open-policy-agent/opa/internal/uuid"
"github.com/open-policy-agent/opa/v1/keys"
"github.com/open-policy-agent/opa/v1/logging"
)
const (
// Default to s3 when the service for sigv4 signing is not specified for backwards compatibility
awsSigv4SigningDefaultService = "s3"
// Default to urn:ietf:params:oauth:client-assertion-type:jwt-bearer for ClientAssertionType when not specified
defaultClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
)
// DefaultTLSConfig defines standard TLS configurations based on the Config
func DefaultTLSConfig(c Config) (*tls.Config, error) {
t := &tls.Config{}
url, err := url.Parse(c.URL)
if err != nil {
return nil, err
}
if url.Scheme == "https" {
t.InsecureSkipVerify = c.AllowInsecureTLS
}
if c.TLS != nil && c.TLS.CACert != "" {
caCert, err := os.ReadFile(c.TLS.CACert)
if err != nil {
return nil, err
}
var rootCAs *x509.CertPool
if c.TLS.SystemCARequired {
rootCAs, err = x509.SystemCertPool()
if err != nil {
return nil, err
}
} else {
rootCAs = x509.NewCertPool()
}
ok := rootCAs.AppendCertsFromPEM(caCert)
if !ok {
return nil, errors.New("unable to parse and append CA certificate to certificate pool")
}
t.RootCAs = rootCAs
}
return t, nil
}
// DefaultRoundTripperClient is a reasonable set of defaults for HTTP auth plugins
func DefaultRoundTripperClient(t *tls.Config, timeout int64) *http.Client {
// Ensure we use a http.Transport with proper settings: the zero values are not
// a good choice, as they cause leaking connections:
// https://github.com/golang/go/issues/19620
// copy, we don't want to alter the default client's Transport
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.ResponseHeaderTimeout = time.Duration(timeout) * time.Second
tr.TLSClientConfig = t
c := *http.DefaultClient
c.Transport = tr
return &c
}
// defaultAuthPlugin represents baseline 'no auth' behavior if no alternative plugin is specified for a service
type defaultAuthPlugin struct{}
func (*defaultAuthPlugin) NewClient(c Config) (*http.Client, error) {
t, err := DefaultTLSConfig(c)
if err != nil {
return nil, err
}
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
}
func (*defaultAuthPlugin) Prepare(*http.Request) error {
return nil
}
type serverTLSConfig struct {
CACert string `json:"ca_cert,omitempty"`
SystemCARequired bool `json:"system_ca_required,omitempty"`
}
// bearerAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
type bearerAuthPlugin struct {
Token string `json:"token"`
TokenPath string `json:"token_path"`
Scheme string `json:"scheme,omitempty"`
// encode is set to true for the OCIDownloader because
// it expects tokens in plain text but needs them in base64.
encode bool
logger logging.Logger
}
func (ap *bearerAuthPlugin) NewClient(c Config) (*http.Client, error) {
t, err := DefaultTLSConfig(c)
ap.logger = c.logger
if err != nil {
return nil, err
}
if ap.Token != "" && ap.TokenPath != "" {
return nil, errors.New("invalid config: specify a value for either the \"token\" or \"token_path\" field")
}
if ap.Scheme == "" {
ap.Scheme = "Bearer"
}
if c.Type == "oci" {
// Standard rest clients use the bearer token as it is defined in the Config
// but the OCIDownloader needs it encoded to base64 before using to sign a request.
ap.encode = true
}
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
}
func (ap *bearerAuthPlugin) Prepare(req *http.Request) error {
token := ap.Token
if ap.logger == nil {
ap.logger = logging.Get()
}
if ap.TokenPath != "" {
bytes, err := os.ReadFile(ap.TokenPath)
if err != nil {
return err
}
token = strings.TrimSpace(string(bytes))
}
if ap.encode {
token = base64.StdEncoding.EncodeToString([]byte(token))
}
if req.Response != nil && (req.Response.StatusCode == http.StatusPermanentRedirect || req.Response.StatusCode == http.StatusTemporaryRedirect) {
ap.logger.Debug("not attaching authorization header as the response contains a redirect")
} else {
ap.logger.Debug("attaching authorization header")
req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
}
return nil
}
type tokenEndpointResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
type awsKmsKeyConfig struct {
Name string `json:"name"`
Algorithm string `json:"algorithm"`
}
func convertSignatureToBase64(alg string, der []byte) (string, error) {
r, s, derErr := pointsFromDER(der)
if derErr != nil {
return "", fmt.Errorf("failed to read points from der %v", derErr)
}
signatureData, err := convertPointsToBase64(alg, r.Bytes(), s.Bytes())
if err != nil {
return "", err
}
return signatureData, nil
}
func pointsFromDER(der []byte) (R, S *big.Int, err error) { //nolint:gocritic
R, S = &big.Int{}, &big.Int{}
data := asn1.RawValue{}
if _, err := asn1.Unmarshal(der, &data); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshall the signature from DER format %v", err)
}
// https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_ResponseSyntax
// https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3
// The format of our DER string is 0x02 + rlen + r + 0x02 + slen + s
rLen := data.Bytes[1] // The entire length of R + offset of 2 for 0x02 and rlen
r := data.Bytes[2 : rLen+2]
// Ignore the next 0x02 and slen bytes and just take the start of S to the end of the byte array
s := data.Bytes[rLen+4:]
R.SetBytes(r)
S.SetBytes(s)
return
}
func convertPointsToBase64(alg string, r, s []byte) (string, error) {
curveBits, err := retrieveCurveBits(alg)
if err != nil {
return "", err
}
keyBytes := curveBits / 8
if curveBits%8 > 0 {
keyBytes++
}
// We serialize the outputs (r and s) into big-endian byte arrays and pad
// them with zeros on the left to make sure the sizes work out. Both arrays
// must be keyBytes long, and the output must be 2*keyBytes long.
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(r):], r)
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(s):], s)
signatureEnc := append(rBytesPadded, sBytesPadded...)
return base64.RawURLEncoding.EncodeToString(signatureEnc), nil
}
func retrieveCurveBits(alg string) (int, error) {
var curveBits int
switch alg {
case "ECDSA_SHA_256":
curveBits = 256
case "ECDSA_SHA_384":
curveBits = 384
case "ECDSA_SHA_512":
curveBits = 512
default:
return 0, fmt.Errorf("unsupported sign algorithm %s", alg)
}
return curveBits, nil
}
func messageDigest(message []byte, alg string) ([]byte, error) {
var digest hash.Hash
switch alg {
case "ECDSA_SHA_256":
digest = sha256.New()
case "ECDSA_SHA_384":
digest = sha512.New384()
case "ECDSA_SHA_512":
digest = sha512.New()
default:
return []byte{}, fmt.Errorf("unsupported sign algorithm %s", alg)
}
digest.Write(message)
return digest.Sum(nil), nil
}
// oauth2ClientCredentialsAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
// obtained through the OAuth2 client credentials flow
type oauth2ClientCredentialsAuthPlugin struct {
GrantType string `json:"grant_type"`
TokenURL string `json:"token_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
SigningKeyID string `json:"signing_key"`
Thumbprint string `json:"thumbprint"`
Claims map[string]interface{} `json:"additional_claims"`
IncludeJti bool `json:"include_jti_claim"`
Scopes []string `json:"scopes,omitempty"`
AdditionalHeaders map[string]string `json:"additional_headers,omitempty"`
AdditionalParameters map[string]string `json:"additional_parameters,omitempty"`
AWSKmsKey *awsKmsKeyConfig `json:"aws_kms,omitempty"`
AWSSigningPlugin *awsSigningAuthPlugin `json:"aws_signing,omitempty"`
ClientAssertionType string `json:"client_assertion_type"`
ClientAssertion string `json:"client_assertion"`
ClientAssertionPath string `json:"client_assertion_path"`
signingKey *keys.Config
signingKeyParsed interface{}
tokenCache *oauth2Token
tlsSkipVerify bool
logger logging.Logger
}
type oauth2Token struct {
Token string
ExpiresAt time.Time
}
func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(ctx context.Context, extClaims map[string]interface{}, signingKey interface{}) (*string, error) {
now := time.Now()
claims := map[string]interface{}{
"iat": now.Unix(),
"exp": now.Add(10 * time.Minute).Unix(),
}
for k, v := range extClaims {
claims[k] = v
}
if len(ap.Scopes) > 0 {
claims["scope"] = strings.Join(ap.Scopes, " ")
}
if ap.IncludeJti {
jti, err := uuid.New(rand.Reader)
if err != nil {
return nil, err
}
claims["jti"] = jti
}
payload, err := json.Marshal(claims)
if err != nil {
return nil, err
}
var jwsHeaders []byte
var signatureAlg string
if ap.AWSKmsKey == nil {
signatureAlg = ap.signingKey.Algorithm
} else {
signatureAlg, err = ap.mapKMSAlgToSign(ap.AWSKmsKey.Algorithm)
if err != nil {
return nil, err
}
}
if ap.Thumbprint != "" {
bytes, err := hex.DecodeString(ap.Thumbprint)
if err != nil {
return nil, err
}
x5t := base64.URLEncoding.EncodeToString(bytes)
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s","x5t":"%s"}`, signatureAlg, x5t))
} else {
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s"}`, signatureAlg))
}
var jwsCompact []byte
if ap.AWSKmsKey == nil {
jwsCompact, err = jws.SignLiteral(payload,
jwa.SignatureAlgorithm(signatureAlg),
signingKey,
jwsHeaders,
rand.Reader)
} else {
jwsCompact, err = ap.SignWithKMS(ctx, payload, jwsHeaders)
}
if err != nil {
return nil, err
}
jwt := string(jwsCompact)
return &jwt, nil
}
func (ap *oauth2ClientCredentialsAuthPlugin) mapKMSAlgToSign(alg string) (string, error) {
switch alg {
case "ECDSA_SHA_256":
return "ES256", nil
case "ECDSA_SHA_384":
return "ES384", nil
case "ECDSA_SHA_512":
return "ES512", nil
default:
return "", fmt.Errorf("unsupported sign algorithm %s", alg)
}
}
// SignWithKMS will sign the JWT in AWS using the key stored in the supplied kmsArn
func (ap *oauth2ClientCredentialsAuthPlugin) SignWithKMS(ctx context.Context, payload []byte, hdrBuf []byte) ([]byte, error) {
encodedHdr := base64.RawURLEncoding.EncodeToString(hdrBuf)
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
input := encodedHdr + "." + encodedPayload
digest, err := messageDigest([]byte(input), ap.AWSKmsKey.Algorithm)
if err != nil {
return nil, err
}
if ap.AWSSigningPlugin != nil {
signature, err := ap.AWSSigningPlugin.SignDigest(ctx, digest, ap.AWSKmsKey.Name, ap.AWSKmsKey.Algorithm)
if err != nil {
return nil, err
}
der, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return nil, err
}
signatureData, err := convertSignatureToBase64(ap.AWSKmsKey.Algorithm, der)
if err != nil {
return nil, err
}
signedAssertion := input + "." + signatureData
return []byte(signedAssertion), nil
}
return nil, errors.New("missing AWS credentials, failed to sign the assertion with kms")
}
func (ap *oauth2ClientCredentialsAuthPlugin) parseSigningKey(c Config) (err error) {
if ap.SigningKeyID == "" {
return errors.New("signing_key required for jwt_bearer grant type")
}
if val, ok := c.keys[ap.SigningKeyID]; ok {
if val.PrivateKey == "" {
return errors.New("referenced signing_key does not include a private key")
}
ap.signingKey = val
} else {
return errors.New("signing_key refers to non-existent key")
}
alg := jwa.SignatureAlgorithm(ap.signingKey.Algorithm)
ap.signingKeyParsed, err = sign.GetSigningKey(ap.signingKey.PrivateKey, alg)
if err != nil {
return err
}
return nil
}
func (ap *oauth2ClientCredentialsAuthPlugin) NewClient(c Config) (*http.Client, error) {
t, err := DefaultTLSConfig(c)
if err != nil {
return nil, err
}
if ap.GrantType == "" {
// Use client_credentials as default to not break existing config
ap.GrantType = grantTypeClientCredentials
} else if ap.GrantType != grantTypeClientCredentials && ap.GrantType != grantTypeJwtBearer {
return nil, errors.New("grant_type must be either client_credentials or jwt_bearer")
}
if ap.GrantType == grantTypeJwtBearer || (ap.GrantType == grantTypeClientCredentials && ap.SigningKeyID != "") {
if err = ap.parseSigningKey(c); err != nil {
return nil, err
}
}
// Inherit skip verify from the "parent" settings. Should this be configurable on the credentials too?
ap.tlsSkipVerify = c.AllowInsecureTLS
ap.logger = c.logger
if !strings.HasPrefix(ap.TokenURL, "https://") {
return nil, errors.New("token_url required to use https scheme")
}
if ap.GrantType == grantTypeClientCredentials {
clientCredentialExists := make(map[string]bool)
clientCredentialExists["client_secret"] = ap.ClientSecret != ""
clientCredentialExists["signing_key"] = ap.SigningKeyID != ""
clientCredentialExists["aws_kms"] = ap.AWSKmsKey != nil
clientCredentialExists["client_assertion"] = ap.ClientAssertion != ""
clientCredentialExists["client_assertion_path"] = ap.ClientAssertionPath != ""
var notEmptyVarCount int
for _, credentialSet := range clientCredentialExists {
if credentialSet {
notEmptyVarCount++
}
}
if notEmptyVarCount == 0 {
return nil, errors.New("please provide one of client_secret, signing_key, aws_kms, client_assertion, or client_assertion_path required")
}
if notEmptyVarCount > 1 {
return nil, errors.New("can only use one of client_secret, signing_key, aws_kms, client_assertion, or client_assertion_path")
}
if clientCredentialExists["aws_kms"] {
if ap.AWSSigningPlugin == nil {
return nil, errors.New("aws_kms and aws_signing required")
}
// initialize the awsSigningAuthPlugin
_, err = ap.AWSSigningPlugin.NewClient(c)
if err != nil {
return nil, err
}
} else if clientCredentialExists["client_assertion"] {
if ap.ClientAssertionType == "" {
ap.ClientAssertionType = defaultClientAssertionType
}
if ap.ClientID == "" {
return nil, errors.New("client_id and client_assertion required")
}
} else if clientCredentialExists["client_assertion_path"] {
if ap.ClientAssertionType == "" {
ap.ClientAssertionType = defaultClientAssertionType
}
if ap.ClientID == "" {
return nil, errors.New("client_id and client_assertion_path required")
}
} else if clientCredentialExists["client_secret"] {
if ap.ClientID == "" {
return nil, errors.New("client_id and client_secret required")
}
}
}
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
}
// requestToken tries to obtain an access token using either the client credentials flow
// https://tools.ietf.org/html/rfc6749#section-4.4
// or the JWT authorization grant
// https://tools.ietf.org/html/rfc7523
func (ap *oauth2ClientCredentialsAuthPlugin) requestToken(ctx context.Context) (*oauth2Token, error) {
body := url.Values{}
if ap.GrantType == grantTypeJwtBearer {
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
if err != nil {
return nil, err
}
body.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
body.Add("assertion", *authJwt)
} else {
body.Add("grant_type", grantTypeClientCredentials)
if ap.SigningKeyID != "" || ap.AWSKmsKey != nil {
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
if err != nil {
return nil, err
}
body.Add("client_assertion_type", defaultClientAssertionType)
body.Add("client_assertion", *authJwt)
if ap.ClientID != "" {
body.Add("client_id", ap.ClientID)
}
} else if ap.ClientAssertion != "" {
if ap.ClientAssertionType == "" {
ap.ClientAssertionType = defaultClientAssertionType
}
if ap.ClientID != "" {
body.Add("client_id", ap.ClientID)
}
body.Add("client_assertion_type", ap.ClientAssertionType)
body.Add("client_assertion", ap.ClientAssertion)
} else if ap.ClientAssertionPath != "" {
if ap.ClientAssertionType == "" {
ap.ClientAssertionType = defaultClientAssertionType
}
bytes, err := os.ReadFile(ap.ClientAssertionPath)
if err != nil {
return nil, err
}
if ap.ClientID != "" {
body.Add("client_id", ap.ClientID)
}
body.Add("client_assertion_type", ap.ClientAssertionType)
body.Add("client_assertion", strings.TrimSpace(string(bytes)))
}
}
if len(ap.Scopes) > 0 {
body.Add("scope", strings.Join(ap.Scopes, " "))
}
for k, v := range ap.AdditionalParameters {
body.Set(k, v)
}
r, err := http.NewRequestWithContext(ctx, "POST", ap.TokenURL, strings.NewReader(body.Encode()))
if err != nil {
return nil, err
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if ap.GrantType == grantTypeClientCredentials && ap.ClientSecret != "" {
r.SetBasicAuth(ap.ClientID, ap.ClientSecret)
}
for k, v := range ap.AdditionalHeaders {
r.Header.Add(k, v)
}
client := DefaultRoundTripperClient(&tls.Config{InsecureSkipVerify: ap.tlsSkipVerify}, 10)
response, err := client.Do(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
bodyRaw, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, fmt.Errorf("error in response from OAuth2 token endpoint: %v", string(bodyRaw))
}
var tokenResponse tokenEndpointResponse
err = json.Unmarshal(bodyRaw, &tokenResponse)
if err != nil {
return nil, err
}
if !strings.EqualFold(tokenResponse.TokenType, "bearer") {
return nil, errors.New("unknown token type returned from token endpoint")
}
return &oauth2Token{
Token: strings.TrimSpace(tokenResponse.AccessToken),
ExpiresAt: time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second),
}, nil
}
func (ap *oauth2ClientCredentialsAuthPlugin) Prepare(req *http.Request) error {
minTokenLifetime := float64(10)
if ap.tokenCache == nil || time.Until(ap.tokenCache.ExpiresAt).Seconds() < minTokenLifetime {
ap.logger.Debug("Requesting token from token_url %v", ap.TokenURL)
token, err := ap.requestToken(req.Context())
if err != nil {
return err
}
ap.tokenCache = token
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", ap.tokenCache.Token))
return nil
}
// clientTLSAuthPlugin represents authentication via client certificate on a TLS connection
type clientTLSAuthPlugin struct {
Cert string `json:"cert"`
PrivateKey string `json:"private_key"`
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
CACert string `json:"ca_cert,omitempty"` // Deprecated: Use `services[_].tls.ca_cert` instead
SystemCARequired bool `json:"system_ca_required,omitempty"` // Deprecated: Use `services[_].tls.system_ca_required` instead
}
func (ap *clientTLSAuthPlugin) NewClient(c Config) (*http.Client, error) {
tlsConfig, err := DefaultTLSConfig(c)
if err != nil {
return nil, err
}
if ap.Cert == "" {
return nil, errors.New("client certificate is needed when client TLS is enabled")
}
if ap.PrivateKey == "" {
return nil, errors.New("private key is needed when client TLS is enabled")
}
var keyPEMBlock []byte
data, err := os.ReadFile(ap.PrivateKey)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("PEM data could not be found")
}
// nolint: staticcheck // We don't want to forbid users from using this encryption.
if x509.IsEncryptedPEMBlock(block) {
if ap.PrivateKeyPassphrase == "" {
return nil, errors.New("client certificate passphrase is needed, because the certificate is password encrypted")
}
// nolint: staticcheck // We don't want to forbid users from using this encryption.
block, err := x509.DecryptPEMBlock(block, []byte(ap.PrivateKeyPassphrase))
if err != nil {
return nil, err
}
key, err := x509.ParsePKCS8PrivateKey(block)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(block)
if err != nil {
return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err)
}
}
rsa, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("private key is invalid")
}
keyPEMBlock = pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(rsa),
},
)
} else {
keyPEMBlock = data
}
certPEMBlock, err := os.ReadFile(ap.Cert)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
var client *http.Client
if c.TLS != nil && c.TLS.CACert != "" {
client = DefaultRoundTripperClient(tlsConfig, *c.ResponseHeaderTimeoutSeconds)
} else {
if ap.CACert != "" {
c.logger.Warn("Deprecated 'services[_].credentials.client_tls.ca_cert' configuration specified. Use 'services[_].tls.ca_cert' instead. See https://www.openpolicyagent.org/docs/latest/configuration/#services")
caCert, err := os.ReadFile(ap.CACert)
if err != nil {
return nil, err
}
var caCertPool *x509.CertPool
if ap.SystemCARequired {
caCertPool, err = x509.SystemCertPool()
if err != nil {
return nil, err
}
} else {
caCertPool = x509.NewCertPool()
}
ok := caCertPool.AppendCertsFromPEM(caCert)
if !ok {
return nil, errors.New("unable to parse and append CA certificate to certificate pool")
}
tlsConfig.RootCAs = caCertPool
}
client = DefaultRoundTripperClient(tlsConfig, *c.ResponseHeaderTimeoutSeconds)
}
return client, nil
}
func (ap *clientTLSAuthPlugin) Prepare(_ *http.Request) error {
return nil
}
// awsSigningAuthPlugin represents authentication using AWS V4 HMAC signing in the Authorization header
type awsSigningAuthPlugin struct {
AWSEnvironmentCredentials *awsEnvironmentCredentialService `json:"environment_credentials,omitempty"`
AWSMetadataCredentials *awsMetadataCredentialService `json:"metadata_credentials,omitempty"`
AWSAssumeRoleCredentials *awsAssumeRoleCredentialService `json:"assume_role_credentials,omitempty"`
AWSWebIdentityCredentials *awsWebIdentityCredentialService `json:"web_identity_credentials,omitempty"`
AWSProfileCredentials *awsProfileCredentialService `json:"profile_credentials,omitempty"`
AWSService string `json:"service,omitempty"`
AWSSignatureVersion string `json:"signature_version,omitempty"`
host string
ecrAuthPlugin *ecrAuthPlugin
kmsSignPlugin *awsKMSSignPlugin
logger logging.Logger
}
type awsCredentialServiceChain struct {
awsCredentialServices []awsCredentialService
logger logging.Logger
}
func (acs *awsCredentialServiceChain) addService(service awsCredentialService) {
acs.awsCredentialServices = append(acs.awsCredentialServices, service)
}
type awsCredentialCheckErrors []*awsCredentialCheckError
func (e awsCredentialCheckErrors) Error() string {
if len(e) == 0 {
return "no error(s)"
}
if len(e) == 1 {
return fmt.Sprintf("1 error occurred: %v", e[0].Error())
}
s := make([]string, len(e))
for i, err := range e {
s[i] = err.Error()
}
return fmt.Sprintf("%d errors occurred:\n%s", len(e), strings.Join(s, "\n"))
}
type awsCredentialCheckError struct {
message string
}
func newAWSCredentialError(message string) *awsCredentialCheckError {
return &awsCredentialCheckError{
message: message,
}
}
func (e *awsCredentialCheckError) Error() string {
return e.message
}
func (acs *awsCredentialServiceChain) credentials(ctx context.Context) (aws.Credentials, error) {
var errs awsCredentialCheckErrors
for _, service := range acs.awsCredentialServices {
credential, err := service.credentials(ctx)
if err != nil {
acs.logger.Debug("awsSigningAuthPlugin:%T failed: %v", service, err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return aws.Credentials{}, err
}
errs = append(errs, newAWSCredentialError(err.Error()))
continue
}
acs.logger.Debug("awsSigningAuthPlugin:%T successful", service)
return credential, nil
}
return aws.Credentials{}, fmt.Errorf("all AWS credential providers failed: %v", errs)
}
func (ap *awsSigningAuthPlugin) awsCredentialService() awsCredentialService {
chain := awsCredentialServiceChain{
logger: ap.logger,
}
/*
Here we maintain the order of addition to the chain inline with
the order of credential providers followed by default by the
AWS SDK. For example
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
*/
if ap.AWSEnvironmentCredentials != nil {
ap.AWSEnvironmentCredentials.logger = ap.logger
chain.addService(ap.AWSEnvironmentCredentials)
}
if ap.AWSAssumeRoleCredentials != nil {
ap.AWSAssumeRoleCredentials.logger = ap.logger
chain.addService(ap.AWSAssumeRoleCredentials)
}
if ap.AWSWebIdentityCredentials != nil {
ap.AWSWebIdentityCredentials.logger = ap.logger
chain.addService(ap.AWSWebIdentityCredentials)
}
if ap.AWSProfileCredentials != nil {
ap.AWSProfileCredentials.logger = ap.logger
chain.addService(ap.AWSProfileCredentials)
}
if ap.AWSMetadataCredentials != nil {
ap.AWSMetadataCredentials.logger = ap.logger
chain.addService(ap.AWSMetadataCredentials)
}
return &chain
}
func (ap *awsSigningAuthPlugin) NewClient(c Config) (*http.Client, error) {
t, err := DefaultTLSConfig(c)
if err != nil {
return nil, err
}
url, err := url.Parse(c.URL)
if err != nil {
return nil, err
}
ap.host = url.Host
if ap.logger == nil {
ap.logger = c.logger
}
if err := ap.validateAndSetDefaults(c.Type); err != nil {
return nil, err
}
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
}
func (ap *awsSigningAuthPlugin) Prepare(req *http.Request) error {
if ap.host != req.URL.Host {
// Return early if the host does not match.
// This can happen when the OCI registry responded with a redirect to another host.
// For instance, ECR redirects to S3 and the ECR auth header should not be included in the S3 request.
return nil
}
switch ap.AWSService {
case "ecr":
return ap.ecrAuthPlugin.Prepare(req)
default:
creds, err := ap.awsCredentialService().credentials(req.Context())
if err != nil {
return fmt.Errorf("failed to get aws credentials: %w", err)
}
ap.logger.Debug("Signing request with AWS credentials.")
return aws.SignRequest(req, ap.AWSService, creds, time.Now(), ap.AWSSignatureVersion)
}
}
func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error {
cfgs := map[bool]int{}
cfgs[ap.AWSEnvironmentCredentials != nil]++
cfgs[ap.AWSMetadataCredentials != nil]++
cfgs[ap.AWSAssumeRoleCredentials != nil]++
cfgs[ap.AWSWebIdentityCredentials != nil]++
cfgs[ap.AWSProfileCredentials != nil]++
if cfgs[true] == 0 {
return errors.New("a AWS credential service must be specified when S3 signing is enabled")
}
if ap.AWSMetadataCredentials != nil {
if ap.AWSMetadataCredentials.RegionName == "" {
return errors.New("at least aws_region must be specified for AWS metadata credential service")
}
}
if ap.AWSAssumeRoleCredentials != nil {
if err := ap.AWSAssumeRoleCredentials.populateFromEnv(); err != nil {
return err
}
}
if ap.AWSWebIdentityCredentials != nil {
if err := ap.AWSWebIdentityCredentials.populateFromEnv(); err != nil {
return err
}
}
ap.AWSService = strings.ToLower(ap.AWSService)
// Only allow ECR for OCI service types
if serviceType == "oci" {
if ap.AWSService == "" {
ap.AWSService = "ecr"
}
if ap.AWSService != "ecr" {
return fmt.Errorf(`cannot use aws service %q with service type "oci"`, ap.AWSService)
}
// We need to setup a special auth plugin for ECR.
ap.ecrAuthPlugin = newECRAuthPlugin(ap)
} else {
// Disallow ECR for non-OCI service types
if ap.AWSService == "ecr" {
return errors.New(`aws service "ecr" must be used with service type "oci"`)
}
if ap.AWSService == "kms" && ap.kmsSignPlugin == nil {
// We need a special plugin for KMS.
ap.kmsSignPlugin = newKMSSignPlugin(ap)
}
if ap.AWSService == "" {
ap.AWSService = awsSigv4SigningDefaultService
}
}
if ap.AWSSignatureVersion == "" {
ap.AWSSignatureVersion = "4"
}
return nil
}
func (ap *awsSigningAuthPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
switch ap.AWSService {
case "kms":
return ap.kmsSignPlugin.SignDigest(ctx, digest, keyID, signingAlgorithm)
default:
return "", fmt.Errorf(`cannot use SignDigest with aws service %q`, ap.AWSService)
}
}