diff --git a/go.mod b/go.mod index 19873c679..476d70bee 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,8 @@ require ( require ( github.com/apache/arrow-go/v18 v18.3.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.6 // indirect + github.com/aws/smithy-go v1.25.0 // indirect github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -160,3 +162,5 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/smartcontractkit/chainlink-protos/cre/go => ../chainlink-protos/cre/go diff --git a/go.sum b/go.sum index 0918c4f25..6596b3ee6 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,10 @@ github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c h1:cxQVoh6kY+c4b0HUchHjGWBI8288VhH50qxKG3hdEg0= github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c/go.mod h1:3XzxudkrYVUvbduN/uI2fl4lSrMSzU0+3RCu2mpnfx8= +github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= +github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= +github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= +github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -340,8 +344,6 @@ github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260409211238-5b99921cbc7c h1:7V+V3R//Z/g1kiz/to9MGT4KmcSTcbv2YSsRDD0RN5A= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260409211238-5b99921cbc7c/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b h1:36knUpKHHAZ86K4FGWXtx8i/EQftGdk2bqCoEu/Cha8= diff --git a/pkg/capabilities/v2/actions/confidentialhttp/client.pb.go b/pkg/capabilities/v2/actions/confidentialhttp/client.pb.go index 4497ce5c8..67e9f7229 100644 --- a/pkg/capabilities/v2/actions/confidentialhttp/client.pb.go +++ b/pkg/capabilities/v2/actions/confidentialhttp/client.pb.go @@ -23,6 +23,52 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type HmacCustom_Hash int32 + +const ( + HmacCustom_HASH_SHA256 HmacCustom_Hash = 0 + HmacCustom_HASH_SHA512 HmacCustom_Hash = 1 +) + +// Enum value maps for HmacCustom_Hash. +var ( + HmacCustom_Hash_name = map[int32]string{ + 0: "HASH_SHA256", + 1: "HASH_SHA512", + } + HmacCustom_Hash_value = map[string]int32{ + "HASH_SHA256": 0, + "HASH_SHA512": 1, + } +) + +func (x HmacCustom_Hash) Enum() *HmacCustom_Hash { + p := new(HmacCustom_Hash) + *p = x + return p +} + +func (x HmacCustom_Hash) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HmacCustom_Hash) Descriptor() protoreflect.EnumDescriptor { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_enumTypes[0].Descriptor() +} + +func (HmacCustom_Hash) Type() protoreflect.EnumType { + return &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_enumTypes[0] +} + +func (x HmacCustom_Hash) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HmacCustom_Hash.Descriptor instead. +func (HmacCustom_Hash) EnumDescriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{12, 0} +} + type SecretIdentifier struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -352,8 +398,15 @@ type ConfidentialHTTPRequest struct { state protoimpl.MessageState `protogen:"open.v1"` VaultDonSecrets []*SecretIdentifier `protobuf:"bytes,1,rep,name=vault_don_secrets,json=vaultDonSecrets,proto3" json:"vault_don_secrets,omitempty"` Request *HTTPRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // auth, when set, instructs the enclave to sign the outbound request using + // the specified method. Every secret_name referenced inside AuthConfig must + // also appear in vault_don_secrets (enforced by the capability validator). + // When auth is nil, the request is sent with only the headers/body the + // caller provided — preserving the legacy "{{.SECRET_NAME}}" header-template + // behavior. + Auth *AuthConfig `protobuf:"bytes,3,opt,name=auth,proto3,oneof" json:"auth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConfidentialHTTPRequest) Reset() { @@ -400,115 +453,1229 @@ func (x *ConfidentialHTTPRequest) GetRequest() *HTTPRequest { return nil } -var File_capabilities_networking_confidentialhttp_v1alpha_client_proto protoreflect.FileDescriptor +func (x *ConfidentialHTTPRequest) GetAuth() *AuthConfig { + if x != nil { + return x.Auth + } + return nil +} -const file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc = "" + - "\n" + - "=capabilities/networking/confidentialhttp/v1alpha/client.proto\x120capabilities.networking.confidentialhttp.v1alpha\x1a\x1egoogle/protobuf/duration.proto\x1a*tools/generator/v1alpha/cre_metadata.proto\"g\n" + - "\x10SecretIdentifier\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x1c\n" + - "\tnamespace\x18\x02 \x01(\tR\tnamespace\x12\x19\n" + - "\x05owner\x18\x03 \x01(\tH\x00R\x05owner\x88\x01\x01B\b\n" + - "\x06_owner\"&\n" + - "\fHeaderValues\x12\x16\n" + - "\x06values\x18\x01 \x03(\tR\x06values\"\xe5\x05\n" + - "\vHTTPRequest\x12\x10\n" + - "\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n" + - "\x06method\x18\x02 \x01(\tR\x06method\x12!\n" + - "\vbody_string\x18\x03 \x01(\tH\x00R\n" + - "bodyString\x12\x1f\n" + - "\n" + - "body_bytes\x18\b \x01(\fH\x00R\tbodyBytes\x12t\n" + - "\rmulti_headers\x18\x04 \x03(\v2O.capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntryR\fmultiHeaders\x12\x8d\x01\n" + - "\x16template_public_values\x18\x05 \x03(\v2W.capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.TemplatePublicValuesEntryR\x14templatePublicValues\x124\n" + - "\x17custom_root_ca_cert_pem\x18\x06 \x01(\fR\x13customRootCaCertPem\x123\n" + - "\atimeout\x18\a \x01(\v2\x19.google.protobuf.DurationR\atimeout\x12%\n" + - "\x0eencrypt_output\x18\t \x01(\bR\rencryptOutput\x1a\x7f\n" + - "\x11MultiHeadersEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12T\n" + - "\x05value\x18\x02 \x01(\v2>.capabilities.networking.confidentialhttp.v1alpha.HeaderValuesR\x05value:\x028\x01\x1aG\n" + - "\x19TemplatePublicValuesEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" + - "\x04body\"\xbb\x02\n" + - "\fHTTPResponse\x12\x1f\n" + - "\vstatus_code\x18\x01 \x01(\rR\n" + - "statusCode\x12\x12\n" + - "\x04body\x18\x02 \x01(\fR\x04body\x12u\n" + - "\rmulti_headers\x18\x03 \x03(\v2P.capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntryR\fmultiHeaders\x1a\x7f\n" + - "\x11MultiHeadersEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12T\n" + - "\x05value\x18\x02 \x01(\v2>.capabilities.networking.confidentialhttp.v1alpha.HeaderValuesR\x05value:\x028\x01\"\xe2\x01\n" + - "\x17ConfidentialHTTPRequest\x12n\n" + - "\x11vault_don_secrets\x18\x01 \x03(\v2B.capabilities.networking.confidentialhttp.v1alpha.SecretIdentifierR\x0fvaultDonSecrets\x12W\n" + - "\arequest\x18\x02 \x01(\v2=.capabilities.networking.confidentialhttp.v1alpha.HTTPRequestR\arequest2\xca\x01\n" + - "\x06Client\x12\x98\x01\n" + - "\vSendRequest\x12I.capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest\x1a>.capabilities.networking.confidentialhttp.v1alpha.HTTPResponse\x1a%\x82\xb5\x18!\b\x01\x12\x1dconfidential-http@1.0.0-alphab\x06proto3" +// AuthConfig selects one of the supported request-signing methods. +// Each variant carries the method-specific parameters and references +// the names of the Vault-DON secrets it needs. +type AuthConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Method: + // + // *AuthConfig_ApiKey + // *AuthConfig_Basic + // *AuthConfig_Bearer + // *AuthConfig_Hmac + // *AuthConfig_Oauth2 + Method isAuthConfig_Method `protobuf_oneof:"method"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -var ( - file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescOnce sync.Once - file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescData []byte -) +func (x *AuthConfig) Reset() { + *x = AuthConfig{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -func file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP() []byte { - file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescOnce.Do(func() { - file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc), len(file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc))) - }) - return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescData +func (x *AuthConfig) String() string { + return protoimpl.X.MessageStringOf(x) } -var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes = make([]protoimpl.MessageInfo, 8) -var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_goTypes = []any{ - (*SecretIdentifier)(nil), // 0: capabilities.networking.confidentialhttp.v1alpha.SecretIdentifier - (*HeaderValues)(nil), // 1: capabilities.networking.confidentialhttp.v1alpha.HeaderValues - (*HTTPRequest)(nil), // 2: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest - (*HTTPResponse)(nil), // 3: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse - (*ConfidentialHTTPRequest)(nil), // 4: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest - nil, // 5: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntry - nil, // 6: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.TemplatePublicValuesEntry - nil, // 7: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntry - (*durationpb.Duration)(nil), // 8: google.protobuf.Duration +func (*AuthConfig) ProtoMessage() {} + +func (x *AuthConfig) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_depIdxs = []int32{ - 5, // 0: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.multi_headers:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntry - 6, // 1: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.template_public_values:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.TemplatePublicValuesEntry - 8, // 2: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.timeout:type_name -> google.protobuf.Duration - 7, // 3: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.multi_headers:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntry - 0, // 4: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest.vault_don_secrets:type_name -> capabilities.networking.confidentialhttp.v1alpha.SecretIdentifier - 2, // 5: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest.request:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPRequest - 1, // 6: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntry.value:type_name -> capabilities.networking.confidentialhttp.v1alpha.HeaderValues - 1, // 7: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntry.value:type_name -> capabilities.networking.confidentialhttp.v1alpha.HeaderValues - 4, // 8: capabilities.networking.confidentialhttp.v1alpha.Client.SendRequest:input_type -> capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest - 3, // 9: capabilities.networking.confidentialhttp.v1alpha.Client.SendRequest:output_type -> capabilities.networking.confidentialhttp.v1alpha.HTTPResponse - 9, // [9:10] is the sub-list for method output_type - 8, // [8:9] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + +// Deprecated: Use AuthConfig.ProtoReflect.Descriptor instead. +func (*AuthConfig) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{5} } -func init() { file_capabilities_networking_confidentialhttp_v1alpha_client_proto_init() } -func file_capabilities_networking_confidentialhttp_v1alpha_client_proto_init() { - if File_capabilities_networking_confidentialhttp_v1alpha_client_proto != nil { - return +func (x *AuthConfig) GetMethod() isAuthConfig_Method { + if x != nil { + return x.Method } - file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[0].OneofWrappers = []any{} - file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[2].OneofWrappers = []any{ - (*HTTPRequest_BodyString)(nil), - (*HTTPRequest_BodyBytes)(nil), + return nil +} + +func (x *AuthConfig) GetApiKey() *ApiKeyAuth { + if x != nil { + if x, ok := x.Method.(*AuthConfig_ApiKey); ok { + return x.ApiKey + } + } + return nil +} + +func (x *AuthConfig) GetBasic() *BasicAuth { + if x != nil { + if x, ok := x.Method.(*AuthConfig_Basic); ok { + return x.Basic + } + } + return nil +} + +func (x *AuthConfig) GetBearer() *BearerAuth { + if x != nil { + if x, ok := x.Method.(*AuthConfig_Bearer); ok { + return x.Bearer + } + } + return nil +} + +func (x *AuthConfig) GetHmac() *HmacAuth { + if x != nil { + if x, ok := x.Method.(*AuthConfig_Hmac); ok { + return x.Hmac + } + } + return nil +} + +func (x *AuthConfig) GetOauth2() *OAuth2Auth { + if x != nil { + if x, ok := x.Method.(*AuthConfig_Oauth2); ok { + return x.Oauth2 + } + } + return nil +} + +type isAuthConfig_Method interface { + isAuthConfig_Method() +} + +type AuthConfig_ApiKey struct { + ApiKey *ApiKeyAuth `protobuf:"bytes,1,opt,name=api_key,json=apiKey,proto3,oneof"` +} + +type AuthConfig_Basic struct { + Basic *BasicAuth `protobuf:"bytes,2,opt,name=basic,proto3,oneof"` +} + +type AuthConfig_Bearer struct { + Bearer *BearerAuth `protobuf:"bytes,3,opt,name=bearer,proto3,oneof"` +} + +type AuthConfig_Hmac struct { + Hmac *HmacAuth `protobuf:"bytes,4,opt,name=hmac,proto3,oneof"` +} + +type AuthConfig_Oauth2 struct { + Oauth2 *OAuth2Auth `protobuf:"bytes,5,opt,name=oauth2,proto3,oneof"` +} + +func (*AuthConfig_ApiKey) isAuthConfig_Method() {} + +func (*AuthConfig_Basic) isAuthConfig_Method() {} + +func (*AuthConfig_Bearer) isAuthConfig_Method() {} + +func (*AuthConfig_Hmac) isAuthConfig_Method() {} + +func (*AuthConfig_Oauth2) isAuthConfig_Method() {} + +// ApiKeyAuth attaches a secret value to a chosen header. +// Renders as: : +// Example (header_name="x-api-key", secret_name="coingecko", value_prefix=""): +// +// x-api-key: +type ApiKeyAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + HeaderName string `protobuf:"bytes,1,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` // required, e.g. "x-api-key", "Authorization" + SecretName string `protobuf:"bytes,2,opt,name=secret_name,json=secretName,proto3" json:"secret_name,omitempty"` // required; key into ConfidentialHTTPRequest.vault_don_secrets + ValuePrefix string `protobuf:"bytes,3,opt,name=value_prefix,json=valuePrefix,proto3" json:"value_prefix,omitempty"` // optional, e.g. "ApiKey ", "Token " + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiKeyAuth) Reset() { + *x = ApiKeyAuth{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiKeyAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiKeyAuth) ProtoMessage() {} + +func (x *ApiKeyAuth) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiKeyAuth.ProtoReflect.Descriptor instead. +func (*ApiKeyAuth) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{6} +} + +func (x *ApiKeyAuth) GetHeaderName() string { + if x != nil { + return x.HeaderName + } + return "" +} + +func (x *ApiKeyAuth) GetSecretName() string { + if x != nil { + return x.SecretName + } + return "" +} + +func (x *ApiKeyAuth) GetValuePrefix() string { + if x != nil { + return x.ValuePrefix + } + return "" +} + +// BasicAuth renders: Authorization: Basic base64(username ":" password). +type BasicAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + UsernameSecretName string `protobuf:"bytes,1,opt,name=username_secret_name,json=usernameSecretName,proto3" json:"username_secret_name,omitempty"` // required + PasswordSecretName string `protobuf:"bytes,2,opt,name=password_secret_name,json=passwordSecretName,proto3" json:"password_secret_name,omitempty"` // required + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BasicAuth) Reset() { + *x = BasicAuth{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BasicAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BasicAuth) ProtoMessage() {} + +func (x *BasicAuth) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BasicAuth.ProtoReflect.Descriptor instead. +func (*BasicAuth) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{7} +} + +func (x *BasicAuth) GetUsernameSecretName() string { + if x != nil { + return x.UsernameSecretName + } + return "" +} + +func (x *BasicAuth) GetPasswordSecretName() string { + if x != nil { + return x.PasswordSecretName + } + return "" +} + +// BearerAuth attaches a pre-issued long-lived token. +// Default behavior: Authorization: Bearer +// header_name and value_prefix allow rare overrides (e.g. GitHub's +// "Authorization: token "). +type BearerAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + TokenSecretName string `protobuf:"bytes,1,opt,name=token_secret_name,json=tokenSecretName,proto3" json:"token_secret_name,omitempty"` // required + HeaderName string `protobuf:"bytes,2,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` // optional override, default "Authorization" + ValuePrefix string `protobuf:"bytes,3,opt,name=value_prefix,json=valuePrefix,proto3" json:"value_prefix,omitempty"` // optional override, default "Bearer " + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BearerAuth) Reset() { + *x = BearerAuth{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BearerAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BearerAuth) ProtoMessage() {} + +func (x *BearerAuth) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BearerAuth.ProtoReflect.Descriptor instead. +func (*BearerAuth) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{8} +} + +func (x *BearerAuth) GetTokenSecretName() string { + if x != nil { + return x.TokenSecretName + } + return "" +} + +func (x *BearerAuth) GetHeaderName() string { + if x != nil { + return x.HeaderName + } + return "" +} + +func (x *BearerAuth) GetValuePrefix() string { + if x != nil { + return x.ValuePrefix + } + return "" +} + +// HmacAuth groups all HMAC-family signing variants. +type HmacAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Variant: + // + // *HmacAuth_Sha256 + // *HmacAuth_AwsSigV4 + // *HmacAuth_Custom + Variant isHmacAuth_Variant `protobuf_oneof:"variant"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HmacAuth) Reset() { + *x = HmacAuth{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HmacAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HmacAuth) ProtoMessage() {} + +func (x *HmacAuth) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HmacAuth.ProtoReflect.Descriptor instead. +func (*HmacAuth) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{9} +} + +func (x *HmacAuth) GetVariant() isHmacAuth_Variant { + if x != nil { + return x.Variant + } + return nil +} + +func (x *HmacAuth) GetSha256() *HmacSha256 { + if x != nil { + if x, ok := x.Variant.(*HmacAuth_Sha256); ok { + return x.Sha256 + } + } + return nil +} + +func (x *HmacAuth) GetAwsSigV4() *AwsSigV4 { + if x != nil { + if x, ok := x.Variant.(*HmacAuth_AwsSigV4); ok { + return x.AwsSigV4 + } + } + return nil +} + +func (x *HmacAuth) GetCustom() *HmacCustom { + if x != nil { + if x, ok := x.Variant.(*HmacAuth_Custom); ok { + return x.Custom + } + } + return nil +} + +type isHmacAuth_Variant interface { + isHmacAuth_Variant() +} + +type HmacAuth_Sha256 struct { + Sha256 *HmacSha256 `protobuf:"bytes,1,opt,name=sha256,proto3,oneof"` +} + +type HmacAuth_AwsSigV4 struct { + AwsSigV4 *AwsSigV4 `protobuf:"bytes,2,opt,name=aws_sig_v4,json=awsSigV4,proto3,oneof"` +} + +type HmacAuth_Custom struct { + Custom *HmacCustom `protobuf:"bytes,3,opt,name=custom,proto3,oneof"` +} + +func (*HmacAuth_Sha256) isHmacAuth_Variant() {} + +func (*HmacAuth_AwsSigV4) isHmacAuth_Variant() {} + +func (*HmacAuth_Custom) isHmacAuth_Variant() {} + +// HmacSha256 implements a generic canonical-string HMAC-SHA256 signature. +// Canonical string: method "\n" url "\n" sha256(body) "\n" timestamp +// Signature is attached via signature_header, timestamp via timestamp_header. +type HmacSha256 struct { + state protoimpl.MessageState `protogen:"open.v1"` + SecretName string `protobuf:"bytes,1,opt,name=secret_name,json=secretName,proto3" json:"secret_name,omitempty"` // required shared secret + SignatureHeader string `protobuf:"bytes,2,opt,name=signature_header,json=signatureHeader,proto3" json:"signature_header,omitempty"` // default "X-Signature" + TimestampHeader string `protobuf:"bytes,3,opt,name=timestamp_header,json=timestampHeader,proto3" json:"timestamp_header,omitempty"` // default "X-Timestamp" + IncludeQuery bool `protobuf:"varint,4,opt,name=include_query,json=includeQuery,proto3" json:"include_query,omitempty"` // if true, include the query string in the canonical url + Encoding string `protobuf:"bytes,5,opt,name=encoding,proto3" json:"encoding,omitempty"` // "hex" (default) or "base64" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HmacSha256) Reset() { + *x = HmacSha256{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HmacSha256) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HmacSha256) ProtoMessage() {} + +func (x *HmacSha256) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HmacSha256.ProtoReflect.Descriptor instead. +func (*HmacSha256) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{10} +} + +func (x *HmacSha256) GetSecretName() string { + if x != nil { + return x.SecretName + } + return "" +} + +func (x *HmacSha256) GetSignatureHeader() string { + if x != nil { + return x.SignatureHeader + } + return "" +} + +func (x *HmacSha256) GetTimestampHeader() string { + if x != nil { + return x.TimestampHeader + } + return "" +} + +func (x *HmacSha256) GetIncludeQuery() bool { + if x != nil { + return x.IncludeQuery + } + return false +} + +func (x *HmacSha256) GetEncoding() string { + if x != nil { + return x.Encoding + } + return "" +} + +// AwsSigV4 implements AWS Signature Version 4. +// Uses github.com/aws/aws-sdk-go-v2/aws/signer/v4 under the hood. +type AwsSigV4 struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessKeyIdSecretName string `protobuf:"bytes,1,opt,name=access_key_id_secret_name,json=accessKeyIdSecretName,proto3" json:"access_key_id_secret_name,omitempty"` // required + SecretAccessKeySecretName string `protobuf:"bytes,2,opt,name=secret_access_key_secret_name,json=secretAccessKeySecretName,proto3" json:"secret_access_key_secret_name,omitempty"` // required + SessionTokenSecretName string `protobuf:"bytes,3,opt,name=session_token_secret_name,json=sessionTokenSecretName,proto3" json:"session_token_secret_name,omitempty"` // optional (for STS creds) + Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"` // required, e.g. "us-east-1" + Service string `protobuf:"bytes,5,opt,name=service,proto3" json:"service,omitempty"` // required, e.g. "execute-api", "s3" + // Signed headers (comma-separated lowercase). Optional; defaults to + // "host;x-amz-date" plus "x-amz-security-token" if session_token_secret_name + // is set. + SignedHeaders []string `protobuf:"bytes,6,rep,name=signed_headers,json=signedHeaders,proto3" json:"signed_headers,omitempty"` + // If true, uses X-Amz-Content-Sha256: UNSIGNED-PAYLOAD (useful for large S3 + // uploads). Default false. + UnsignedPayload bool `protobuf:"varint,7,opt,name=unsigned_payload,json=unsignedPayload,proto3" json:"unsigned_payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AwsSigV4) Reset() { + *x = AwsSigV4{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AwsSigV4) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AwsSigV4) ProtoMessage() {} + +func (x *AwsSigV4) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AwsSigV4.ProtoReflect.Descriptor instead. +func (*AwsSigV4) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{11} +} + +func (x *AwsSigV4) GetAccessKeyIdSecretName() string { + if x != nil { + return x.AccessKeyIdSecretName + } + return "" +} + +func (x *AwsSigV4) GetSecretAccessKeySecretName() string { + if x != nil { + return x.SecretAccessKeySecretName + } + return "" +} + +func (x *AwsSigV4) GetSessionTokenSecretName() string { + if x != nil { + return x.SessionTokenSecretName + } + return "" +} + +func (x *AwsSigV4) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *AwsSigV4) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *AwsSigV4) GetSignedHeaders() []string { + if x != nil { + return x.SignedHeaders + } + return nil +} + +func (x *AwsSigV4) GetUnsignedPayload() bool { + if x != nil { + return x.UnsignedPayload + } + return false +} + +// HmacCustom lets workflow authors specify the canonical string via a +// Go text/template string. Available template vars: +// +// {{.method}} {{.url}} {{.path}} {{.query}} {{.body}} {{.body_sha256}} +// {{.timestamp}} {{.nonce}} {{header "X-Foo"}} +type HmacCustom struct { + state protoimpl.MessageState `protogen:"open.v1"` + SecretName string `protobuf:"bytes,1,opt,name=secret_name,json=secretName,proto3" json:"secret_name,omitempty"` // required + CanonicalTemplate string `protobuf:"bytes,2,opt,name=canonical_template,json=canonicalTemplate,proto3" json:"canonical_template,omitempty"` // required + Hash HmacCustom_Hash `protobuf:"varint,3,opt,name=hash,proto3,enum=capabilities.networking.confidentialhttp.v1alpha.HmacCustom_Hash" json:"hash,omitempty"` + SignatureHeader string `protobuf:"bytes,4,opt,name=signature_header,json=signatureHeader,proto3" json:"signature_header,omitempty"` // required + SignaturePrefix string `protobuf:"bytes,5,opt,name=signature_prefix,json=signaturePrefix,proto3" json:"signature_prefix,omitempty"` // e.g. "HMAC-SHA256 " + TimestampHeader string `protobuf:"bytes,6,opt,name=timestamp_header,json=timestampHeader,proto3" json:"timestamp_header,omitempty"` // optional; if set, timestamp header injected + NonceHeader string `protobuf:"bytes,7,opt,name=nonce_header,json=nonceHeader,proto3" json:"nonce_header,omitempty"` // optional; if set, random nonce header injected + Encoding string `protobuf:"bytes,8,opt,name=encoding,proto3" json:"encoding,omitempty"` // "hex" (default) or "base64" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HmacCustom) Reset() { + *x = HmacCustom{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HmacCustom) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HmacCustom) ProtoMessage() {} + +func (x *HmacCustom) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HmacCustom.ProtoReflect.Descriptor instead. +func (*HmacCustom) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{12} +} + +func (x *HmacCustom) GetSecretName() string { + if x != nil { + return x.SecretName + } + return "" +} + +func (x *HmacCustom) GetCanonicalTemplate() string { + if x != nil { + return x.CanonicalTemplate + } + return "" +} + +func (x *HmacCustom) GetHash() HmacCustom_Hash { + if x != nil { + return x.Hash + } + return HmacCustom_HASH_SHA256 +} + +func (x *HmacCustom) GetSignatureHeader() string { + if x != nil { + return x.SignatureHeader + } + return "" +} + +func (x *HmacCustom) GetSignaturePrefix() string { + if x != nil { + return x.SignaturePrefix + } + return "" +} + +func (x *HmacCustom) GetTimestampHeader() string { + if x != nil { + return x.TimestampHeader + } + return "" +} + +func (x *HmacCustom) GetNonceHeader() string { + if x != nil { + return x.NonceHeader + } + return "" +} + +func (x *HmacCustom) GetEncoding() string { + if x != nil { + return x.Encoding + } + return "" +} + +// OAuth2Auth groups headless OAuth 2.0 flows. +// Interactive flows (Authorization Code, PKCE, Device Code) are NOT supported — +// they require browser consent that a headless TEE cannot perform. +type OAuth2Auth struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Variant: + // + // *OAuth2Auth_ClientCredentials + // *OAuth2Auth_RefreshToken + Variant isOAuth2Auth_Variant `protobuf_oneof:"variant"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuth2Auth) Reset() { + *x = OAuth2Auth{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuth2Auth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuth2Auth) ProtoMessage() {} + +func (x *OAuth2Auth) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuth2Auth.ProtoReflect.Descriptor instead. +func (*OAuth2Auth) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{13} +} + +func (x *OAuth2Auth) GetVariant() isOAuth2Auth_Variant { + if x != nil { + return x.Variant + } + return nil +} + +func (x *OAuth2Auth) GetClientCredentials() *OAuth2ClientCredentials { + if x != nil { + if x, ok := x.Variant.(*OAuth2Auth_ClientCredentials); ok { + return x.ClientCredentials + } + } + return nil +} + +func (x *OAuth2Auth) GetRefreshToken() *OAuth2RefreshToken { + if x != nil { + if x, ok := x.Variant.(*OAuth2Auth_RefreshToken); ok { + return x.RefreshToken + } + } + return nil +} + +type isOAuth2Auth_Variant interface { + isOAuth2Auth_Variant() +} + +type OAuth2Auth_ClientCredentials struct { + ClientCredentials *OAuth2ClientCredentials `protobuf:"bytes,1,opt,name=client_credentials,json=clientCredentials,proto3,oneof"` +} + +type OAuth2Auth_RefreshToken struct { + RefreshToken *OAuth2RefreshToken `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3,oneof"` +} + +func (*OAuth2Auth_ClientCredentials) isOAuth2Auth_Variant() {} + +func (*OAuth2Auth_RefreshToken) isOAuth2Auth_Variant() {} + +// OAuth2ClientCredentials: machine-to-machine grant. +// The enclave POSTs to token_url with client_id/client_secret, caches the +// resulting access_token per (workflow_owner, token_url, client_id, scopes), +// and attaches "Authorization: Bearer " to the outbound request. +type OAuth2ClientCredentials struct { + state protoimpl.MessageState `protogen:"open.v1"` + TokenUrl string `protobuf:"bytes,1,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` // required, must be https:// + ClientIdSecretName string `protobuf:"bytes,2,opt,name=client_id_secret_name,json=clientIdSecretName,proto3" json:"client_id_secret_name,omitempty"` // required + ClientSecretSecretName string `protobuf:"bytes,3,opt,name=client_secret_secret_name,json=clientSecretSecretName,proto3" json:"client_secret_secret_name,omitempty"` // required + Scopes []string `protobuf:"bytes,4,rep,name=scopes,proto3" json:"scopes,omitempty"` // optional + Audience string `protobuf:"bytes,5,opt,name=audience,proto3" json:"audience,omitempty"` // optional (Auth0-style) + // "basic_auth" (default) or "request_body" — where to put client creds + // on the token request. + ClientAuthMethod string `protobuf:"bytes,6,opt,name=client_auth_method,json=clientAuthMethod,proto3" json:"client_auth_method,omitempty"` + // Extra form params to send with the token request. + ExtraParams map[string]string `protobuf:"bytes,7,rep,name=extra_params,json=extraParams,proto3" json:"extra_params,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuth2ClientCredentials) Reset() { + *x = OAuth2ClientCredentials{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuth2ClientCredentials) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuth2ClientCredentials) ProtoMessage() {} + +func (x *OAuth2ClientCredentials) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuth2ClientCredentials.ProtoReflect.Descriptor instead. +func (*OAuth2ClientCredentials) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{14} +} + +func (x *OAuth2ClientCredentials) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *OAuth2ClientCredentials) GetClientIdSecretName() string { + if x != nil { + return x.ClientIdSecretName + } + return "" +} + +func (x *OAuth2ClientCredentials) GetClientSecretSecretName() string { + if x != nil { + return x.ClientSecretSecretName + } + return "" +} + +func (x *OAuth2ClientCredentials) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *OAuth2ClientCredentials) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *OAuth2ClientCredentials) GetClientAuthMethod() string { + if x != nil { + return x.ClientAuthMethod + } + return "" +} + +func (x *OAuth2ClientCredentials) GetExtraParams() map[string]string { + if x != nil { + return x.ExtraParams + } + return nil +} + +// OAuth2RefreshToken: the workflow stores a long-lived refresh_token in Vault +// (obtained out-of-band during an interactive consent). The enclave exchanges +// it for an access_token on cache miss. +// +// Important: if the IdP rotates refresh tokens on each exchange, the enclave +// cannot persist the new refresh_token back to Vault. Disable refresh-token +// rotation at the IdP, or prefer client_credentials where possible. +type OAuth2RefreshToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + TokenUrl string `protobuf:"bytes,1,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` // required, must be https:// + RefreshTokenSecretName string `protobuf:"bytes,2,opt,name=refresh_token_secret_name,json=refreshTokenSecretName,proto3" json:"refresh_token_secret_name,omitempty"` // required + ClientIdSecretName string `protobuf:"bytes,3,opt,name=client_id_secret_name,json=clientIdSecretName,proto3" json:"client_id_secret_name,omitempty"` // optional (some IdPs require) + ClientSecretSecretName string `protobuf:"bytes,4,opt,name=client_secret_secret_name,json=clientSecretSecretName,proto3" json:"client_secret_secret_name,omitempty"` // optional (some IdPs require) + Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` // optional + ExtraParams map[string]string `protobuf:"bytes,6,rep,name=extra_params,json=extraParams,proto3" json:"extra_params,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuth2RefreshToken) Reset() { + *x = OAuth2RefreshToken{} + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuth2RefreshToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuth2RefreshToken) ProtoMessage() {} + +func (x *OAuth2RefreshToken) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuth2RefreshToken.ProtoReflect.Descriptor instead. +func (*OAuth2RefreshToken) Descriptor() ([]byte, []int) { + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP(), []int{15} +} + +func (x *OAuth2RefreshToken) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *OAuth2RefreshToken) GetRefreshTokenSecretName() string { + if x != nil { + return x.RefreshTokenSecretName + } + return "" +} + +func (x *OAuth2RefreshToken) GetClientIdSecretName() string { + if x != nil { + return x.ClientIdSecretName + } + return "" +} + +func (x *OAuth2RefreshToken) GetClientSecretSecretName() string { + if x != nil { + return x.ClientSecretSecretName + } + return "" +} + +func (x *OAuth2RefreshToken) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *OAuth2RefreshToken) GetExtraParams() map[string]string { + if x != nil { + return x.ExtraParams + } + return nil +} + +var File_capabilities_networking_confidentialhttp_v1alpha_client_proto protoreflect.FileDescriptor + +const file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc = "" + + "\n" + + "=capabilities/networking/confidentialhttp/v1alpha/client.proto\x120capabilities.networking.confidentialhttp.v1alpha\x1a\x1egoogle/protobuf/duration.proto\x1a*tools/generator/v1alpha/cre_metadata.proto\"g\n" + + "\x10SecretIdentifier\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1c\n" + + "\tnamespace\x18\x02 \x01(\tR\tnamespace\x12\x19\n" + + "\x05owner\x18\x03 \x01(\tH\x00R\x05owner\x88\x01\x01B\b\n" + + "\x06_owner\"&\n" + + "\fHeaderValues\x12\x16\n" + + "\x06values\x18\x01 \x03(\tR\x06values\"\xe5\x05\n" + + "\vHTTPRequest\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n" + + "\x06method\x18\x02 \x01(\tR\x06method\x12!\n" + + "\vbody_string\x18\x03 \x01(\tH\x00R\n" + + "bodyString\x12\x1f\n" + + "\n" + + "body_bytes\x18\b \x01(\fH\x00R\tbodyBytes\x12t\n" + + "\rmulti_headers\x18\x04 \x03(\v2O.capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntryR\fmultiHeaders\x12\x8d\x01\n" + + "\x16template_public_values\x18\x05 \x03(\v2W.capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.TemplatePublicValuesEntryR\x14templatePublicValues\x124\n" + + "\x17custom_root_ca_cert_pem\x18\x06 \x01(\fR\x13customRootCaCertPem\x123\n" + + "\atimeout\x18\a \x01(\v2\x19.google.protobuf.DurationR\atimeout\x12%\n" + + "\x0eencrypt_output\x18\t \x01(\bR\rencryptOutput\x1a\x7f\n" + + "\x11MultiHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12T\n" + + "\x05value\x18\x02 \x01(\v2>.capabilities.networking.confidentialhttp.v1alpha.HeaderValuesR\x05value:\x028\x01\x1aG\n" + + "\x19TemplatePublicValuesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" + + "\x04body\"\xbb\x02\n" + + "\fHTTPResponse\x12\x1f\n" + + "\vstatus_code\x18\x01 \x01(\rR\n" + + "statusCode\x12\x12\n" + + "\x04body\x18\x02 \x01(\fR\x04body\x12u\n" + + "\rmulti_headers\x18\x03 \x03(\v2P.capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntryR\fmultiHeaders\x1a\x7f\n" + + "\x11MultiHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12T\n" + + "\x05value\x18\x02 \x01(\v2>.capabilities.networking.confidentialhttp.v1alpha.HeaderValuesR\x05value:\x028\x01\"\xc2\x02\n" + + "\x17ConfidentialHTTPRequest\x12n\n" + + "\x11vault_don_secrets\x18\x01 \x03(\v2B.capabilities.networking.confidentialhttp.v1alpha.SecretIdentifierR\x0fvaultDonSecrets\x12W\n" + + "\arequest\x18\x02 \x01(\v2=.capabilities.networking.confidentialhttp.v1alpha.HTTPRequestR\arequest\x12U\n" + + "\x04auth\x18\x03 \x01(\v2<.capabilities.networking.confidentialhttp.v1alpha.AuthConfigH\x00R\x04auth\x88\x01\x01B\a\n" + + "\x05_auth\"\xc6\x03\n" + + "\n" + + "AuthConfig\x12W\n" + + "\aapi_key\x18\x01 \x01(\v2<.capabilities.networking.confidentialhttp.v1alpha.ApiKeyAuthH\x00R\x06apiKey\x12S\n" + + "\x05basic\x18\x02 \x01(\v2;.capabilities.networking.confidentialhttp.v1alpha.BasicAuthH\x00R\x05basic\x12V\n" + + "\x06bearer\x18\x03 \x01(\v2<.capabilities.networking.confidentialhttp.v1alpha.BearerAuthH\x00R\x06bearer\x12P\n" + + "\x04hmac\x18\x04 \x01(\v2:.capabilities.networking.confidentialhttp.v1alpha.HmacAuthH\x00R\x04hmac\x12V\n" + + "\x06oauth2\x18\x05 \x01(\v2<.capabilities.networking.confidentialhttp.v1alpha.OAuth2AuthH\x00R\x06oauth2B\b\n" + + "\x06method\"q\n" + + "\n" + + "ApiKeyAuth\x12\x1f\n" + + "\vheader_name\x18\x01 \x01(\tR\n" + + "headerName\x12\x1f\n" + + "\vsecret_name\x18\x02 \x01(\tR\n" + + "secretName\x12!\n" + + "\fvalue_prefix\x18\x03 \x01(\tR\vvaluePrefix\"o\n" + + "\tBasicAuth\x120\n" + + "\x14username_secret_name\x18\x01 \x01(\tR\x12usernameSecretName\x120\n" + + "\x14password_secret_name\x18\x02 \x01(\tR\x12passwordSecretName\"|\n" + + "\n" + + "BearerAuth\x12*\n" + + "\x11token_secret_name\x18\x01 \x01(\tR\x0ftokenSecretName\x12\x1f\n" + + "\vheader_name\x18\x02 \x01(\tR\n" + + "headerName\x12!\n" + + "\fvalue_prefix\x18\x03 \x01(\tR\vvaluePrefix\"\xa1\x02\n" + + "\bHmacAuth\x12V\n" + + "\x06sha256\x18\x01 \x01(\v2<.capabilities.networking.confidentialhttp.v1alpha.HmacSha256H\x00R\x06sha256\x12Z\n" + + "\n" + + "aws_sig_v4\x18\x02 \x01(\v2:.capabilities.networking.confidentialhttp.v1alpha.AwsSigV4H\x00R\bawsSigV4\x12V\n" + + "\x06custom\x18\x03 \x01(\v2<.capabilities.networking.confidentialhttp.v1alpha.HmacCustomH\x00R\x06customB\t\n" + + "\avariant\"\xc4\x01\n" + + "\n" + + "HmacSha256\x12\x1f\n" + + "\vsecret_name\x18\x01 \x01(\tR\n" + + "secretName\x12)\n" + + "\x10signature_header\x18\x02 \x01(\tR\x0fsignatureHeader\x12)\n" + + "\x10timestamp_header\x18\x03 \x01(\tR\x0ftimestampHeader\x12#\n" + + "\rinclude_query\x18\x04 \x01(\bR\fincludeQuery\x12\x1a\n" + + "\bencoding\x18\x05 \x01(\tR\bencoding\"\xc5\x02\n" + + "\bAwsSigV4\x128\n" + + "\x19access_key_id_secret_name\x18\x01 \x01(\tR\x15accessKeyIdSecretName\x12@\n" + + "\x1dsecret_access_key_secret_name\x18\x02 \x01(\tR\x19secretAccessKeySecretName\x129\n" + + "\x19session_token_secret_name\x18\x03 \x01(\tR\x16sessionTokenSecretName\x12\x16\n" + + "\x06region\x18\x04 \x01(\tR\x06region\x12\x18\n" + + "\aservice\x18\x05 \x01(\tR\aservice\x12%\n" + + "\x0esigned_headers\x18\x06 \x03(\tR\rsignedHeaders\x12)\n" + + "\x10unsigned_payload\x18\a \x01(\bR\x0funsignedPayload\"\x9d\x03\n" + + "\n" + + "HmacCustom\x12\x1f\n" + + "\vsecret_name\x18\x01 \x01(\tR\n" + + "secretName\x12-\n" + + "\x12canonical_template\x18\x02 \x01(\tR\x11canonicalTemplate\x12U\n" + + "\x04hash\x18\x03 \x01(\x0e2A.capabilities.networking.confidentialhttp.v1alpha.HmacCustom.HashR\x04hash\x12)\n" + + "\x10signature_header\x18\x04 \x01(\tR\x0fsignatureHeader\x12)\n" + + "\x10signature_prefix\x18\x05 \x01(\tR\x0fsignaturePrefix\x12)\n" + + "\x10timestamp_header\x18\x06 \x01(\tR\x0ftimestampHeader\x12!\n" + + "\fnonce_header\x18\a \x01(\tR\vnonceHeader\x12\x1a\n" + + "\bencoding\x18\b \x01(\tR\bencoding\"(\n" + + "\x04Hash\x12\x0f\n" + + "\vHASH_SHA256\x10\x00\x12\x0f\n" + + "\vHASH_SHA512\x10\x01\"\x80\x02\n" + + "\n" + + "OAuth2Auth\x12z\n" + + "\x12client_credentials\x18\x01 \x01(\v2I.capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentialsH\x00R\x11clientCredentials\x12k\n" + + "\rrefresh_token\x18\x02 \x01(\v2D.capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshTokenH\x00R\frefreshTokenB\t\n" + + "\avariant\"\xc5\x03\n" + + "\x17OAuth2ClientCredentials\x12\x1b\n" + + "\ttoken_url\x18\x01 \x01(\tR\btokenUrl\x121\n" + + "\x15client_id_secret_name\x18\x02 \x01(\tR\x12clientIdSecretName\x129\n" + + "\x19client_secret_secret_name\x18\x03 \x01(\tR\x16clientSecretSecretName\x12\x16\n" + + "\x06scopes\x18\x04 \x03(\tR\x06scopes\x12\x1a\n" + + "\baudience\x18\x05 \x01(\tR\baudience\x12,\n" + + "\x12client_auth_method\x18\x06 \x01(\tR\x10clientAuthMethod\x12}\n" + + "\fextra_params\x18\a \x03(\v2Z.capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentials.ExtraParamsEntryR\vextraParams\x1a>\n" + + "\x10ExtraParamsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xac\x03\n" + + "\x12OAuth2RefreshToken\x12\x1b\n" + + "\ttoken_url\x18\x01 \x01(\tR\btokenUrl\x129\n" + + "\x19refresh_token_secret_name\x18\x02 \x01(\tR\x16refreshTokenSecretName\x121\n" + + "\x15client_id_secret_name\x18\x03 \x01(\tR\x12clientIdSecretName\x129\n" + + "\x19client_secret_secret_name\x18\x04 \x01(\tR\x16clientSecretSecretName\x12\x16\n" + + "\x06scopes\x18\x05 \x03(\tR\x06scopes\x12x\n" + + "\fextra_params\x18\x06 \x03(\v2U.capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshToken.ExtraParamsEntryR\vextraParams\x1a>\n" + + "\x10ExtraParamsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xca\x01\n" + + "\x06Client\x12\x98\x01\n" + + "\vSendRequest\x12I.capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest\x1a>.capabilities.networking.confidentialhttp.v1alpha.HTTPResponse\x1a%\x82\xb5\x18!\b\x01\x12\x1dconfidential-http@1.0.0-alphab\x06proto3" + +var ( + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescOnce sync.Once + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescData []byte +) + +func file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescGZIP() []byte { + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescOnce.Do(func() { + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc), len(file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc))) + }) + return file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDescData +} + +var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_goTypes = []any{ + (HmacCustom_Hash)(0), // 0: capabilities.networking.confidentialhttp.v1alpha.HmacCustom.Hash + (*SecretIdentifier)(nil), // 1: capabilities.networking.confidentialhttp.v1alpha.SecretIdentifier + (*HeaderValues)(nil), // 2: capabilities.networking.confidentialhttp.v1alpha.HeaderValues + (*HTTPRequest)(nil), // 3: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest + (*HTTPResponse)(nil), // 4: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse + (*ConfidentialHTTPRequest)(nil), // 5: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest + (*AuthConfig)(nil), // 6: capabilities.networking.confidentialhttp.v1alpha.AuthConfig + (*ApiKeyAuth)(nil), // 7: capabilities.networking.confidentialhttp.v1alpha.ApiKeyAuth + (*BasicAuth)(nil), // 8: capabilities.networking.confidentialhttp.v1alpha.BasicAuth + (*BearerAuth)(nil), // 9: capabilities.networking.confidentialhttp.v1alpha.BearerAuth + (*HmacAuth)(nil), // 10: capabilities.networking.confidentialhttp.v1alpha.HmacAuth + (*HmacSha256)(nil), // 11: capabilities.networking.confidentialhttp.v1alpha.HmacSha256 + (*AwsSigV4)(nil), // 12: capabilities.networking.confidentialhttp.v1alpha.AwsSigV4 + (*HmacCustom)(nil), // 13: capabilities.networking.confidentialhttp.v1alpha.HmacCustom + (*OAuth2Auth)(nil), // 14: capabilities.networking.confidentialhttp.v1alpha.OAuth2Auth + (*OAuth2ClientCredentials)(nil), // 15: capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentials + (*OAuth2RefreshToken)(nil), // 16: capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshToken + nil, // 17: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntry + nil, // 18: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.TemplatePublicValuesEntry + nil, // 19: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntry + nil, // 20: capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentials.ExtraParamsEntry + nil, // 21: capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshToken.ExtraParamsEntry + (*durationpb.Duration)(nil), // 22: google.protobuf.Duration +} +var file_capabilities_networking_confidentialhttp_v1alpha_client_proto_depIdxs = []int32{ + 17, // 0: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.multi_headers:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntry + 18, // 1: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.template_public_values:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.TemplatePublicValuesEntry + 22, // 2: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.timeout:type_name -> google.protobuf.Duration + 19, // 3: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.multi_headers:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntry + 1, // 4: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest.vault_don_secrets:type_name -> capabilities.networking.confidentialhttp.v1alpha.SecretIdentifier + 3, // 5: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest.request:type_name -> capabilities.networking.confidentialhttp.v1alpha.HTTPRequest + 6, // 6: capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest.auth:type_name -> capabilities.networking.confidentialhttp.v1alpha.AuthConfig + 7, // 7: capabilities.networking.confidentialhttp.v1alpha.AuthConfig.api_key:type_name -> capabilities.networking.confidentialhttp.v1alpha.ApiKeyAuth + 8, // 8: capabilities.networking.confidentialhttp.v1alpha.AuthConfig.basic:type_name -> capabilities.networking.confidentialhttp.v1alpha.BasicAuth + 9, // 9: capabilities.networking.confidentialhttp.v1alpha.AuthConfig.bearer:type_name -> capabilities.networking.confidentialhttp.v1alpha.BearerAuth + 10, // 10: capabilities.networking.confidentialhttp.v1alpha.AuthConfig.hmac:type_name -> capabilities.networking.confidentialhttp.v1alpha.HmacAuth + 14, // 11: capabilities.networking.confidentialhttp.v1alpha.AuthConfig.oauth2:type_name -> capabilities.networking.confidentialhttp.v1alpha.OAuth2Auth + 11, // 12: capabilities.networking.confidentialhttp.v1alpha.HmacAuth.sha256:type_name -> capabilities.networking.confidentialhttp.v1alpha.HmacSha256 + 12, // 13: capabilities.networking.confidentialhttp.v1alpha.HmacAuth.aws_sig_v4:type_name -> capabilities.networking.confidentialhttp.v1alpha.AwsSigV4 + 13, // 14: capabilities.networking.confidentialhttp.v1alpha.HmacAuth.custom:type_name -> capabilities.networking.confidentialhttp.v1alpha.HmacCustom + 0, // 15: capabilities.networking.confidentialhttp.v1alpha.HmacCustom.hash:type_name -> capabilities.networking.confidentialhttp.v1alpha.HmacCustom.Hash + 15, // 16: capabilities.networking.confidentialhttp.v1alpha.OAuth2Auth.client_credentials:type_name -> capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentials + 16, // 17: capabilities.networking.confidentialhttp.v1alpha.OAuth2Auth.refresh_token:type_name -> capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshToken + 20, // 18: capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentials.extra_params:type_name -> capabilities.networking.confidentialhttp.v1alpha.OAuth2ClientCredentials.ExtraParamsEntry + 21, // 19: capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshToken.extra_params:type_name -> capabilities.networking.confidentialhttp.v1alpha.OAuth2RefreshToken.ExtraParamsEntry + 2, // 20: capabilities.networking.confidentialhttp.v1alpha.HTTPRequest.MultiHeadersEntry.value:type_name -> capabilities.networking.confidentialhttp.v1alpha.HeaderValues + 2, // 21: capabilities.networking.confidentialhttp.v1alpha.HTTPResponse.MultiHeadersEntry.value:type_name -> capabilities.networking.confidentialhttp.v1alpha.HeaderValues + 5, // 22: capabilities.networking.confidentialhttp.v1alpha.Client.SendRequest:input_type -> capabilities.networking.confidentialhttp.v1alpha.ConfidentialHTTPRequest + 4, // 23: capabilities.networking.confidentialhttp.v1alpha.Client.SendRequest:output_type -> capabilities.networking.confidentialhttp.v1alpha.HTTPResponse + 23, // [23:24] is the sub-list for method output_type + 22, // [22:23] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name +} + +func init() { file_capabilities_networking_confidentialhttp_v1alpha_client_proto_init() } +func file_capabilities_networking_confidentialhttp_v1alpha_client_proto_init() { + if File_capabilities_networking_confidentialhttp_v1alpha_client_proto != nil { + return + } + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[0].OneofWrappers = []any{} + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[2].OneofWrappers = []any{ + (*HTTPRequest_BodyString)(nil), + (*HTTPRequest_BodyBytes)(nil), + } + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[4].OneofWrappers = []any{} + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[5].OneofWrappers = []any{ + (*AuthConfig_ApiKey)(nil), + (*AuthConfig_Basic)(nil), + (*AuthConfig_Bearer)(nil), + (*AuthConfig_Hmac)(nil), + (*AuthConfig_Oauth2)(nil), + } + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[9].OneofWrappers = []any{ + (*HmacAuth_Sha256)(nil), + (*HmacAuth_AwsSigV4)(nil), + (*HmacAuth_Custom)(nil), + } + file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes[13].OneofWrappers = []any{ + (*OAuth2Auth_ClientCredentials)(nil), + (*OAuth2Auth_RefreshToken)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc), len(file_capabilities_networking_confidentialhttp_v1alpha_client_proto_rawDesc)), - NumEnums: 0, - NumMessages: 8, + NumEnums: 1, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, GoTypes: file_capabilities_networking_confidentialhttp_v1alpha_client_proto_goTypes, DependencyIndexes: file_capabilities_networking_confidentialhttp_v1alpha_client_proto_depIdxs, + EnumInfos: file_capabilities_networking_confidentialhttp_v1alpha_client_proto_enumTypes, MessageInfos: file_capabilities_networking_confidentialhttp_v1alpha_client_proto_msgTypes, }.Build() File_capabilities_networking_confidentialhttp_v1alpha_client_proto = out.File diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/apikey.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/apikey.go new file mode 100644 index 000000000..64bd4419e --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/apikey.go @@ -0,0 +1,41 @@ +package signer + +import ( + "context" + "errors" + "net/http" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +type apiKeySigner struct { + headerName string + secretName string + valuePrefix string +} + +func newAPIKeySigner(cfg *confhttppb.ApiKeyAuth) (Signer, error) { + if cfg == nil { + return nil, errors.New("api_key auth config is nil") + } + if cfg.GetHeaderName() == "" { + return nil, errors.New("api_key: header_name is required") + } + if cfg.GetSecretName() == "" { + return nil, errors.New("api_key: secret_name is required") + } + return &apiKeySigner{ + headerName: cfg.GetHeaderName(), + secretName: cfg.GetSecretName(), + valuePrefix: cfg.GetValuePrefix(), + }, nil +} + +func (s *apiKeySigner) Sign(_ context.Context, req *http.Request, secrets map[string]string) error { + val, err := resolveSecret(secrets, s.secretName) + if err != nil { + return err + } + req.Header.Set(s.headerName, s.valuePrefix+val) + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/aws_sigv4.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/aws_sigv4.go new file mode 100644 index 000000000..72143ff3e --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/aws_sigv4.go @@ -0,0 +1,96 @@ +package signer + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +// unsignedPayload is the magic payload-hash value recognized by services +// that support streaming uploads without hashing the body (notably S3). +const unsignedPayload = "UNSIGNED-PAYLOAD" + +type awsSigV4Signer struct { + accessKeyIDSecretName string + secretAccessKeySecretName string + sessionTokenSecretName string // optional + region string + service string + unsignedPayload bool + signer *v4.Signer + nowFn func() time.Time +} + +func newAwsSigV4Signer(cfg *confhttppb.AwsSigV4) (Signer, error) { + if cfg == nil { + return nil, errors.New("aws_sig_v4 config is nil") + } + if cfg.GetAccessKeyIdSecretName() == "" || cfg.GetSecretAccessKeySecretName() == "" { + return nil, errors.New("aws_sig_v4: access_key_id_secret_name and secret_access_key_secret_name are required") + } + if cfg.GetRegion() == "" { + return nil, errors.New("aws_sig_v4: region is required") + } + if cfg.GetService() == "" { + return nil, errors.New("aws_sig_v4: service is required") + } + return &awsSigV4Signer{ + accessKeyIDSecretName: cfg.GetAccessKeyIdSecretName(), + secretAccessKeySecretName: cfg.GetSecretAccessKeySecretName(), + sessionTokenSecretName: cfg.GetSessionTokenSecretName(), + region: cfg.GetRegion(), + service: cfg.GetService(), + unsignedPayload: cfg.GetUnsignedPayload(), + signer: v4.NewSigner(), + nowFn: time.Now, + }, nil +} + +func (s *awsSigV4Signer) Sign(ctx context.Context, req *http.Request, secrets map[string]string) error { + ak, err := resolveSecret(secrets, s.accessKeyIDSecretName) + if err != nil { + return err + } + sk, err := resolveSecret(secrets, s.secretAccessKeySecretName) + if err != nil { + return err + } + creds := aws.Credentials{ + AccessKeyID: ak, + SecretAccessKey: sk, + } + if s.sessionTokenSecretName != "" { + st, err := resolveSecret(secrets, s.sessionTokenSecretName) + if err != nil { + return err + } + creds.SessionToken = st + } + + var payloadHash string + if s.unsignedPayload { + payloadHash = unsignedPayload + // S3 and other services that accept UNSIGNED-PAYLOAD require the + // caller to explicitly advertise that choice via + // X-Amz-Content-Sha256 — the signer does not set it for us. + req.Header.Set("X-Amz-Content-Sha256", unsignedPayload) + } else { + body, err := readBodyForHashing(req) + if err != nil { + return fmt.Errorf("%w: read body: %v", ErrSigV4Sign, err) + } + payloadHash = sha256Hex(body) + } + + if err := s.signer.SignHTTP(ctx, creds, req, payloadHash, s.service, s.region, s.nowFn()); err != nil { + return fmt.Errorf("%w: %v", ErrSigV4Sign, err) + } + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/basic.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/basic.go new file mode 100644 index 000000000..79faf4785 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/basic.go @@ -0,0 +1,42 @@ +package signer + +import ( + "context" + "encoding/base64" + "errors" + "net/http" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +type basicSigner struct { + usernameSecretName string + passwordSecretName string +} + +func newBasicSigner(cfg *confhttppb.BasicAuth) (Signer, error) { + if cfg == nil { + return nil, errors.New("basic auth config is nil") + } + if cfg.GetUsernameSecretName() == "" || cfg.GetPasswordSecretName() == "" { + return nil, errors.New("basic: username_secret_name and password_secret_name are required") + } + return &basicSigner{ + usernameSecretName: cfg.GetUsernameSecretName(), + passwordSecretName: cfg.GetPasswordSecretName(), + }, nil +} + +func (s *basicSigner) Sign(_ context.Context, req *http.Request, secrets map[string]string) error { + u, err := resolveSecret(secrets, s.usernameSecretName) + if err != nil { + return err + } + p, err := resolveSecret(secrets, s.passwordSecretName) + if err != nil { + return err + } + creds := base64.StdEncoding.EncodeToString([]byte(u + ":" + p)) + req.Header.Set("Authorization", "Basic "+creds) + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/bearer.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/bearer.go new file mode 100644 index 000000000..e2cb33d05 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/bearer.go @@ -0,0 +1,46 @@ +package signer + +import ( + "context" + "errors" + "net/http" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +type bearerSigner struct { + secretName string + headerName string + valuePrefix string +} + +func newBearerSigner(cfg *confhttppb.BearerAuth) (Signer, error) { + if cfg == nil { + return nil, errors.New("bearer auth config is nil") + } + if cfg.GetTokenSecretName() == "" { + return nil, errors.New("bearer: token_secret_name is required") + } + h := cfg.GetHeaderName() + if h == "" { + h = "Authorization" + } + p := cfg.GetValuePrefix() + if p == "" { + p = "Bearer " + } + return &bearerSigner{ + secretName: cfg.GetTokenSecretName(), + headerName: h, + valuePrefix: p, + }, nil +} + +func (s *bearerSigner) Sign(_ context.Context, req *http.Request, secrets map[string]string) error { + tok, err := resolveSecret(secrets, s.secretName) + if err != nil { + return err + } + req.Header.Set(s.headerName, s.valuePrefix+tok) + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/encoding.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/encoding.go new file mode 100644 index 000000000..c7b898de9 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/encoding.go @@ -0,0 +1,52 @@ +package signer + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "io" + "net/http" +) + +func toHex(b []byte) string { + return hex.EncodeToString(b) +} + +func toBase64(b []byte) string { + return base64.StdEncoding.EncodeToString(b) +} + +// readBodyForHashing reads and rewinds req.Body so it can be signed and then +// re-sent by http.Client. Returns the raw body bytes. +// +// Go sets req.GetBody automatically for bodies created via bytes.NewBuffer +// and similar; we rely on that to restore the body for the second read. If +// GetBody is unset and Body is consumed, subsequent Do() would send an empty +// body. Callers must ensure GetBody is set before invoking any HMAC/SigV4 +// signer. +func readBodyForHashing(req *http.Request) ([]byte, error) { + if req.Body == nil || req.Body == http.NoBody { + return nil, nil + } + b, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + if closer, ok := req.Body.(io.Closer); ok { + _ = closer.Close() + } + // Restore body for the actual send. + if req.GetBody != nil { + nb, gerr := req.GetBody() + if gerr != nil { + return nil, gerr + } + req.Body = nb + } + return b, nil +} + +func sha256Hex(b []byte) string { + h := sha256.Sum256(b) + return toHex(h[:]) +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/errors.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/errors.go new file mode 100644 index 000000000..58ef43061 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/errors.go @@ -0,0 +1,69 @@ +package signer + +import "errors" + +// Error sentinels emitted by signers. These are matched by the framework +// executor's string-sentinel-to-caperrors mapping to classify the error's +// visibility and code (see confidential-compute/capabilities/framework/ +// executor.go). +// +// Sentinels here represent *signer-layer* failures. Validation-layer failures +// (e.g. missing required fields, secret-name not in vault_don_secrets) are +// returned from the confidentialhttp validator. +var ( + // ErrSecretNameEmpty means an auth variant referenced a secret name + // that was the empty string. This should never happen if the validator + // ran first. + ErrSecretNameEmpty = errors.New("confidentialhttp signer: secret name is empty") + + // ErrSecretNotProvided means the workflow did not include a value for + // a secret name that the auth config requires. The validator should + // have rejected the request earlier. + ErrSecretNotProvided = errors.New("confidentialhttp signer: required secret not provided") + + // ErrSecretEmpty means the Vault-DON returned an empty value for a + // required secret. + ErrSecretEmpty = errors.New("confidentialhttp signer: required secret is empty") + + // ErrSigV4Sign indicates the AWS SigV4 signer failed. Wrapped with the + // underlying error from aws-sdk-go-v2. + ErrSigV4Sign = errors.New("confidentialhttp signer: AWS SigV4 signing failed") + + // ErrHmacTemplateParse means a user-supplied canonical_template did not + // parse as a valid Go text/template. Surfaced as a user error. + ErrHmacTemplateParse = errors.New("confidentialhttp signer: HMAC canonical template parse failed") + + // ErrHmacTemplateExec means template execution failed at runtime + // (unknown field access, type mismatch, etc.). + ErrHmacTemplateExec = errors.New("confidentialhttp signer: HMAC canonical template execution failed") + + // ErrHmacTemplateTimeout means template execution exceeded the time + // budget (guards against pathological user templates). + ErrHmacTemplateTimeout = errors.New("confidentialhttp signer: HMAC canonical template timed out") + + // ErrOAuth2TokenEndpointUnreachable is returned when the IdP could not + // be reached at all (network error, TLS handshake failure, etc.). + ErrOAuth2TokenEndpointUnreachable = errors.New("confidentialhttp signer: OAuth2 token endpoint unreachable") + + // ErrOAuth2TokenEndpointHTTPError is returned when the IdP returned a + // non-2xx response. The body is NOT surfaced in the error message to + // avoid leaking secrets that some IdPs echo back. + ErrOAuth2TokenEndpointHTTPError = errors.New("confidentialhttp signer: OAuth2 token endpoint returned non-2xx") + + // ErrOAuth2TokenResponseInvalid means the 2xx response from the IdP did + // not parse as a valid OAuth2 access-token response. + ErrOAuth2TokenResponseInvalid = errors.New("confidentialhttp signer: OAuth2 token response invalid") + + // ErrOAuth2TokenURLInvalid means token_url was not https:// or otherwise + // malformed. Validator should have caught this earlier; this is a + // defense-in-depth check. + ErrOAuth2TokenURLInvalid = errors.New("confidentialhttp signer: OAuth2 token URL invalid") + + // ErrUnsupportedHashAlgorithm is returned when HmacCustom specifies a + // hash enum value the signer does not implement. + ErrUnsupportedHashAlgorithm = errors.New("confidentialhttp signer: unsupported hash algorithm") + + // ErrUnsupportedEncoding is returned for an unrecognized "hex"/"base64" + // encoding string. + ErrUnsupportedEncoding = errors.New("confidentialhttp signer: unsupported encoding") +) diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac.go new file mode 100644 index 000000000..151bd658d --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac.go @@ -0,0 +1,39 @@ +package signer + +import ( + "errors" + "fmt" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +func newHmacSigner(cfg *confhttppb.HmacAuth) (Signer, error) { + if cfg == nil { + return nil, errors.New("hmac auth config is nil") + } + switch v := cfg.GetVariant().(type) { + case *confhttppb.HmacAuth_Sha256: + return newHmacSha256Signer(v.Sha256) + case *confhttppb.HmacAuth_AwsSigV4: + return newAwsSigV4Signer(v.AwsSigV4) + case *confhttppb.HmacAuth_Custom: + return newHmacCustomSigner(v.Custom) + case nil: + return nil, errors.New("hmac auth variant not set") + default: + return nil, fmt.Errorf("unsupported hmac variant %T", v) + } +} + +// encode formats a raw MAC digest as hex or base64. +// encoding may be "", "hex" (default), or "base64". +func encodeMAC(mac []byte, encoding string) (string, error) { + switch encoding { + case "", "hex": + return toHex(mac), nil + case "base64": + return toBase64(mac), nil + default: + return "", fmt.Errorf("%w: %q", ErrUnsupportedEncoding, encoding) + } +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac_custom.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac_custom.go new file mode 100644 index 000000000..88c178dc7 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac_custom.go @@ -0,0 +1,198 @@ +package signer + +import ( + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "errors" + "fmt" + "hash" + "net/http" + "strconv" + "strings" + "sync" + "text/template" + "time" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +// hmacTemplateBudget bounds how long a user-supplied canonical template may +// spend executing. Prevents pathological templates (huge repeated loops) from +// stalling signing. +const hmacTemplateBudget = 30 * time.Millisecond + +type hmacCustomSigner struct { + secretName string + tpl *template.Template + hashFactory func() hash.Hash + signatureHeader string + signaturePrefix string + timestampHeader string + nonceHeader string + encoding string + nowFn func() time.Time + nonceFn func() (string, error) +} + +func newHmacCustomSigner(cfg *confhttppb.HmacCustom) (Signer, error) { + if cfg == nil { + return nil, errors.New("hmac_custom config is nil") + } + if cfg.GetSecretName() == "" { + return nil, errors.New("hmac_custom: secret_name is required") + } + if cfg.GetCanonicalTemplate() == "" { + return nil, errors.New("hmac_custom: canonical_template is required") + } + if cfg.GetSignatureHeader() == "" { + return nil, errors.New("hmac_custom: signature_header is required") + } + + var factory func() hash.Hash + switch cfg.GetHash() { + case confhttppb.HmacCustom_HASH_SHA256: + factory = sha256.New + case confhttppb.HmacCustom_HASH_SHA512: + factory = sha512.New + default: + return nil, fmt.Errorf("%w: %v", ErrUnsupportedHashAlgorithm, cfg.GetHash()) + } + + tpl, err := template.New("hmac_canonical"). + Funcs(template.FuncMap{ + "header": func(_ string) string { + // Placeholder — overridden per-request so the template can + // observe mutable req.Header values. Parsing requires the + // func be defined at parse time. + return "" + }, + }). + Parse(cfg.GetCanonicalTemplate()) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrHmacTemplateParse, err) + } + + return &hmacCustomSigner{ + secretName: cfg.GetSecretName(), + tpl: tpl, + hashFactory: factory, + signatureHeader: cfg.GetSignatureHeader(), + signaturePrefix: cfg.GetSignaturePrefix(), + timestampHeader: cfg.GetTimestampHeader(), + nonceHeader: cfg.GetNonceHeader(), + encoding: cfg.GetEncoding(), + nowFn: time.Now, + nonceFn: defaultNonce, + }, nil +} + +func (s *hmacCustomSigner) Sign(ctx context.Context, req *http.Request, secrets map[string]string) error { + secret, err := resolveSecret(secrets, s.secretName) + if err != nil { + return err + } + + body, err := readBodyForHashing(req) + if err != nil { + return err + } + + ts := strconv.FormatInt(s.nowFn().Unix(), 10) + var nonce string + if s.nonceHeader != "" { + nonce, err = s.nonceFn() + if err != nil { + return err + } + } + + data := map[string]any{ + "method": req.Method, + "url": req.URL.String(), + "path": req.URL.Path, + "query": req.URL.RawQuery, + "body": string(body), + "body_sha256": sha256Hex(body), + "timestamp": ts, + "nonce": nonce, + } + + // Re-clone the template to bind a request-scoped `header` func that reads + // actual req.Header values. + tpl, err := s.tpl.Clone() + if err != nil { + return fmt.Errorf("%w: clone: %v", ErrHmacTemplateExec, err) + } + tpl = tpl.Funcs(template.FuncMap{ + "header": func(name string) string { return req.Header.Get(name) }, + }) + + canonical, err := executeTemplateWithBudget(ctx, tpl, data) + if err != nil { + return err + } + + mac := hmac.New(s.hashFactory, []byte(secret)) + mac.Write([]byte(canonical)) + sig, err := encodeMAC(mac.Sum(nil), s.encoding) + if err != nil { + return err + } + + if s.timestampHeader != "" { + req.Header.Set(s.timestampHeader, ts) + } + if s.nonceHeader != "" { + req.Header.Set(s.nonceHeader, nonce) + } + req.Header.Set(s.signatureHeader, s.signaturePrefix+sig) + return nil +} + +// executeTemplateWithBudget runs tpl.Execute with a time budget. If the +// budget is exceeded we return ErrHmacTemplateTimeout. The template itself +// is not interruptible, but this bounds the *detectable* stall — a truly +// runaway template would still pin the goroutine; in practice this is +// extremely unlikely for text/template. +func executeTemplateWithBudget(ctx context.Context, tpl *template.Template, data any) (string, error) { + var ( + out strings.Builder + wg sync.WaitGroup + err error + ) + ctx, cancel := context.WithTimeout(ctx, hmacTemplateBudget) + defer cancel() + + wg.Add(1) + go func() { + defer wg.Done() + err = tpl.Execute(&out, data) + }() + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + + select { + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return "", ErrHmacTemplateTimeout + } + return "", ctx.Err() + case <-done: + if err != nil { + return "", fmt.Errorf("%w: %v", ErrHmacTemplateExec, err) + } + return out.String(), nil + } +} + +func defaultNonce() (string, error) { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + return "", err + } + return hex.EncodeToString(b[:]), nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac_sha256.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac_sha256.go new file mode 100644 index 000000000..9e2ed1cfa --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/hmac_sha256.go @@ -0,0 +1,74 @@ +package signer + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "errors" + "net/http" + "strconv" + "time" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +type hmacSha256Signer struct { + secretName string + signatureHeader string + timestampHeader string + includeQuery bool + encoding string + nowFn func() time.Time +} + +func newHmacSha256Signer(cfg *confhttppb.HmacSha256) (Signer, error) { + if cfg == nil { + return nil, errors.New("hmac_sha256 config is nil") + } + if cfg.GetSecretName() == "" { + return nil, errors.New("hmac_sha256: secret_name is required") + } + sh := cfg.GetSignatureHeader() + if sh == "" { + sh = "X-Signature" + } + th := cfg.GetTimestampHeader() + if th == "" { + th = "X-Timestamp" + } + return &hmacSha256Signer{ + secretName: cfg.GetSecretName(), + signatureHeader: sh, + timestampHeader: th, + includeQuery: cfg.GetIncludeQuery(), + encoding: cfg.GetEncoding(), + nowFn: time.Now, + }, nil +} + +func (s *hmacSha256Signer) Sign(_ context.Context, req *http.Request, secrets map[string]string) error { + secret, err := resolveSecret(secrets, s.secretName) + if err != nil { + return err + } + body, err := readBodyForHashing(req) + if err != nil { + return err + } + url := req.URL.Path + if s.includeQuery && req.URL.RawQuery != "" { + url = url + "?" + req.URL.RawQuery + } + ts := strconv.FormatInt(s.nowFn().Unix(), 10) + canonical := req.Method + "\n" + url + "\n" + sha256Hex(body) + "\n" + ts + + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(canonical)) + sig, err := encodeMAC(mac.Sum(nil), s.encoding) + if err != nil { + return err + } + req.Header.Set(s.timestampHeader, ts) + req.Header.Set(s.signatureHeader, sig) + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2.go new file mode 100644 index 000000000..169ca4ac0 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2.go @@ -0,0 +1,100 @@ +package signer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +// oauth2TokenResponseMaxBytes caps how much of the IdP response we'll read. +// IdPs typically return <4KB; a rogue IdP should not DoS us. +const oauth2TokenResponseMaxBytes = 16 * 1024 + +// clientAuthMethodBasic and clientAuthMethodBody are the two ways OAuth2 +// callers pass client_id/client_secret to the token endpoint. +const ( + clientAuthMethodBasic = "basic_auth" + clientAuthMethodBody = "request_body" +) + +func newOAuth2Signer(cfg *confhttppb.OAuth2Auth, httpClient *http.Client, cache *oauth2Cache) (Signer, error) { + if cfg == nil { + return nil, errors.New("oauth2 auth config is nil") + } + switch v := cfg.GetVariant().(type) { + case *confhttppb.OAuth2Auth_ClientCredentials: + return newOAuth2ClientCredsSigner(v.ClientCredentials, httpClient, cache) + case *confhttppb.OAuth2Auth_RefreshToken: + return newOAuth2RefreshSigner(v.RefreshToken, httpClient, cache) + case nil: + return nil, errors.New("oauth2 variant not set") + default: + return nil, fmt.Errorf("unsupported oauth2 variant %T", v) + } +} + +// tokenResponse is the minimal shape of a successful OAuth2 token exchange. +type tokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` + TokenType string `json:"token_type"` +} + +// postTokenRequest sends form-encoded data to tokenURL and returns the +// parsed response. Classifies failures into the signer error sentinels so +// the framework can map them to the right caperrors code. +func postTokenRequest( + ctx context.Context, + httpClient *http.Client, + tokenURL string, + form url.Values, + basicAuth *struct{ user, pass string }, +) (*tokenResponse, error) { + if !strings.HasPrefix(tokenURL, "https://") { + return nil, fmt.Errorf("%w: %q", ErrOAuth2TokenURLInvalid, tokenURL) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode())) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrOAuth2TokenEndpointUnreachable, err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + if basicAuth != nil { + req.SetBasicAuth(basicAuth.user, basicAuth.pass) + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrOAuth2TokenEndpointUnreachable, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, oauth2TokenResponseMaxBytes)) + if err != nil { + return nil, fmt.Errorf("%w: read body: %v", ErrOAuth2TokenResponseInvalid, err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // Intentionally do NOT include the response body — some IdPs echo + // back client_id or include the invalid token, which would leak + // into workflow error messages. + return nil, fmt.Errorf("%w: status %d", ErrOAuth2TokenEndpointHTTPError, resp.StatusCode) + } + + var tr tokenResponse + if err := json.Unmarshal(body, &tr); err != nil { + return nil, fmt.Errorf("%w: %v", ErrOAuth2TokenResponseInvalid, err) + } + if tr.AccessToken == "" { + return nil, fmt.Errorf("%w: access_token missing", ErrOAuth2TokenResponseInvalid) + } + return &tr, nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_cache.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_cache.go new file mode 100644 index 000000000..2c5103be8 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_cache.go @@ -0,0 +1,146 @@ +package signer + +import ( + "crypto/sha256" + "encoding/hex" + "sort" + "strings" + "sync" + "time" + + "golang.org/x/sync/singleflight" +) + +// oauth2Token is the cached result of a successful token exchange. +type oauth2Token struct { + accessToken string + expiresAt time.Time +} + +// oauth2Cache stores access tokens keyed by a stable fingerprint of the +// request that produced them. Concurrent cache misses for the same key +// collapse to a single upstream call via singleflight. +// +// A 60-second safety margin is subtracted from expires_in to avoid handing +// out a token that's about to expire mid-flight. When the IdP omits +// expires_in, we default to a 5-minute TTL. +type oauth2Cache struct { + mu sync.Mutex + tokens map[string]*oauth2Token + group singleflight.Group + nowFn func() time.Time +} + +func newOAuth2Cache() *oauth2Cache { + return &oauth2Cache{ + tokens: make(map[string]*oauth2Token), + nowFn: time.Now, + } +} + +// get returns a live cached token, or (nil, false) on miss/expiry. +func (c *oauth2Cache) get(key string) (*oauth2Token, bool) { + c.mu.Lock() + defer c.mu.Unlock() + t, ok := c.tokens[key] + if !ok { + return nil, false + } + // now >= expiresAt counts as expired (safer than strict After). + if !c.nowFn().Before(t.expiresAt) { + delete(c.tokens, key) + return nil, false + } + return t, true +} + +// put stores a token. expiresIn is in seconds; safety margin is applied. +func (c *oauth2Cache) put(key, accessToken string, expiresIn int64) { + c.mu.Lock() + defer c.mu.Unlock() + ttl := time.Duration(expiresIn) * time.Second + const safety = 60 * time.Second + const defaultTTL = 5 * time.Minute + if ttl <= 0 { + ttl = defaultTTL + } + effective := ttl - safety + if effective <= 0 { + // IdP returned a TTL at or below the safety margin. Store with + // zero effective TTL so the next get() treats it as expired and + // re-fetches. The alternative (caching for the full TTL) risks + // handing out a token that expires mid-flight. + effective = 0 + } + c.tokens[key] = &oauth2Token{ + accessToken: accessToken, + expiresAt: c.nowFn().Add(effective), + } +} + +// invalidate drops a key. Used when the upstream returns an auth failure +// that suggests the cached token is no longer valid. +func (c *oauth2Cache) invalidate(key string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.tokens, key) +} + +// fetchOrFetch returns a cached token if live; otherwise invokes fetch +// exactly once across concurrent callers (via singleflight) and caches the +// result. +func (c *oauth2Cache) fetchOrFetch( + key string, + fetch func() (accessToken string, expiresIn int64, err error), +) (string, error) { + if t, ok := c.get(key); ok { + return t.accessToken, nil + } + v, err, _ := c.group.Do(key, func() (any, error) { + // Re-check after acquiring singleflight slot in case a sibling + // call already populated the cache. + if t, ok := c.get(key); ok { + return t.accessToken, nil + } + tok, expiresIn, err := fetch() + if err != nil { + return "", err + } + c.put(key, tok, expiresIn) + return tok, nil + }) + if err != nil { + return "", err + } + return v.(string), nil +} + +// cacheKey is a stable fingerprint derived from the parameters that +// determine which access token a caller is eligible to receive. Must be +// stable across invocations for the same logical request, and MUST NOT +// include secret values directly — we hash an identifying fingerprint of +// the refresh_token rather than the token itself, so cache keys are safe +// to log or expose in error messages. +func cacheKey(parts ...string) string { + h := sha256.New() + for _, p := range parts { + h.Write([]byte(p)) + h.Write([]byte{0}) // delimiter to prevent boundary collisions + } + return hex.EncodeToString(h.Sum(nil)) +} + +// sortedJoin produces a deterministic representation of scopes. +func sortedJoin(scopes []string, sep string) string { + cp := append([]string(nil), scopes...) + sort.Strings(cp) + return strings.Join(cp, sep) +} + +// fingerprint returns a short hash of a secret value for use in a cache key. +// Lets us detect "refresh_token changed" without storing the token in the +// key directly. +func fingerprint(s string) string { + h := sha256.Sum256([]byte(s)) + return hex.EncodeToString(h[:8]) +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_client_creds.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_client_creds.go new file mode 100644 index 000000000..565b35251 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_client_creds.go @@ -0,0 +1,107 @@ +package signer + +import ( + "context" + "errors" + "net/http" + "net/url" + "strings" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +type oauth2ClientCredsSigner struct { + tokenURL string + clientIDSecretName string + clientSecretSecretName string + scopes []string + audience string + clientAuthMethod string + extraParams map[string]string + httpClient *http.Client + cache *oauth2Cache +} + +func newOAuth2ClientCredsSigner(cfg *confhttppb.OAuth2ClientCredentials, httpClient *http.Client, cache *oauth2Cache) (Signer, error) { + if cfg == nil { + return nil, errors.New("oauth2 client_credentials config is nil") + } + if cfg.GetTokenUrl() == "" { + return nil, errors.New("oauth2 client_credentials: token_url is required") + } + if cfg.GetClientIdSecretName() == "" || cfg.GetClientSecretSecretName() == "" { + return nil, errors.New("oauth2 client_credentials: client_id_secret_name and client_secret_secret_name are required") + } + method := cfg.GetClientAuthMethod() + if method == "" { + method = clientAuthMethodBasic + } + if method != clientAuthMethodBasic && method != clientAuthMethodBody { + return nil, errors.New("oauth2 client_credentials: client_auth_method must be 'basic_auth' or 'request_body'") + } + return &oauth2ClientCredsSigner{ + tokenURL: cfg.GetTokenUrl(), + clientIDSecretName: cfg.GetClientIdSecretName(), + clientSecretSecretName: cfg.GetClientSecretSecretName(), + scopes: cfg.GetScopes(), + audience: cfg.GetAudience(), + clientAuthMethod: method, + extraParams: cfg.GetExtraParams(), + httpClient: httpClient, + cache: cache, + }, nil +} + +func (s *oauth2ClientCredsSigner) Sign(ctx context.Context, req *http.Request, secrets map[string]string) error { + cid, err := resolveSecret(secrets, s.clientIDSecretName) + if err != nil { + return err + } + csec, err := resolveSecret(secrets, s.clientSecretSecretName) + if err != nil { + return err + } + + key := cacheKey( + "oauth2.client_credentials", + s.tokenURL, + cid, + sortedJoin(s.scopes, " "), + s.audience, + s.clientAuthMethod, + ) + + tok, err := s.cache.fetchOrFetch(key, func() (string, int64, error) { + form := url.Values{} + form.Set("grant_type", "client_credentials") + if len(s.scopes) > 0 { + form.Set("scope", strings.Join(s.scopes, " ")) + } + if s.audience != "" { + form.Set("audience", s.audience) + } + for k, v := range s.extraParams { + form.Set(k, v) + } + + var basic *struct{ user, pass string } + if s.clientAuthMethod == clientAuthMethodBasic { + basic = &struct{ user, pass string }{cid, csec} + } else { + form.Set("client_id", cid) + form.Set("client_secret", csec) + } + + tr, err := postTokenRequest(ctx, s.httpClient, s.tokenURL, form, basic) + if err != nil { + return "", 0, err + } + return tr.AccessToken, tr.ExpiresIn, nil + }) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+tok) + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_refresh.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_refresh.go new file mode 100644 index 000000000..782083f7d --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/oauth2_refresh.go @@ -0,0 +1,108 @@ +package signer + +import ( + "context" + "errors" + "net/http" + "net/url" + "strings" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +type oauth2RefreshSigner struct { + tokenURL string + refreshTokenSecretName string + clientIDSecretName string // optional + clientSecretSecretName string // optional + scopes []string + extraParams map[string]string + httpClient *http.Client + cache *oauth2Cache +} + +func newOAuth2RefreshSigner(cfg *confhttppb.OAuth2RefreshToken, httpClient *http.Client, cache *oauth2Cache) (Signer, error) { + if cfg == nil { + return nil, errors.New("oauth2 refresh_token config is nil") + } + if cfg.GetTokenUrl() == "" { + return nil, errors.New("oauth2 refresh_token: token_url is required") + } + if cfg.GetRefreshTokenSecretName() == "" { + return nil, errors.New("oauth2 refresh_token: refresh_token_secret_name is required") + } + return &oauth2RefreshSigner{ + tokenURL: cfg.GetTokenUrl(), + refreshTokenSecretName: cfg.GetRefreshTokenSecretName(), + clientIDSecretName: cfg.GetClientIdSecretName(), + clientSecretSecretName: cfg.GetClientSecretSecretName(), + scopes: cfg.GetScopes(), + extraParams: cfg.GetExtraParams(), + httpClient: httpClient, + cache: cache, + }, nil +} + +func (s *oauth2RefreshSigner) Sign(ctx context.Context, req *http.Request, secrets map[string]string) error { + rt, err := resolveSecret(secrets, s.refreshTokenSecretName) + if err != nil { + return err + } + + var cid, csec string + if s.clientIDSecretName != "" { + cid, err = resolveSecret(secrets, s.clientIDSecretName) + if err != nil { + return err + } + } + if s.clientSecretSecretName != "" { + csec, err = resolveSecret(secrets, s.clientSecretSecretName) + if err != nil { + return err + } + } + + key := cacheKey( + "oauth2.refresh_token", + s.tokenURL, + cid, + sortedJoin(s.scopes, " "), + fingerprint(rt), + ) + + tok, err := s.cache.fetchOrFetch(key, func() (string, int64, error) { + form := url.Values{} + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", rt) + if len(s.scopes) > 0 { + form.Set("scope", strings.Join(s.scopes, " ")) + } + for k, v := range s.extraParams { + form.Set(k, v) + } + + // When both client_id AND client_secret are present, we send them + // via HTTP Basic Auth (standard). When only client_id is set (PKCE- + // style refresh), include it in the form body. + var basic *struct{ user, pass string } + switch { + case cid != "" && csec != "": + basic = &struct{ user, pass string }{cid, csec} + case cid != "": + form.Set("client_id", cid) + } + + tr, err := postTokenRequest(ctx, s.httpClient, s.tokenURL, form, basic) + if err != nil { + return "", 0, err + } + return tr.AccessToken, tr.ExpiresIn, nil + }) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+tok) + return nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/signer.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/signer.go new file mode 100644 index 000000000..006879820 --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/signer.go @@ -0,0 +1,97 @@ +// Package signer implements request-signing for the confidentialHTTP capability. +// +// Each Signer takes a ready-to-send *http.Request, the values of the Vault-DON +// secrets that the workflow declared, and mutates the request in place to +// attach authentication (header, signature, etc.). +// +// Signers are pure logic: they never talk to Vault DON. Secret resolution +// happens upstream in confidential-compute's framework executor before Sign +// is invoked. The signer receives the already-decrypted values via the +// secrets map. +// +// The OAuth2 signers are the one exception: they make outbound HTTP calls to +// the IdP's token endpoint to exchange credentials for an access token. That +// still does not involve Vault DON — the credentials used for the exchange +// come from the secrets map. +package signer + +import ( + "context" + "errors" + "fmt" + "net/http" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +// Signer mutates httpReq in place to attach authentication material. +// secrets holds secret values keyed by the name the workflow declared in +// ConfidentialHTTPRequest.vault_don_secrets. +type Signer interface { + Sign(ctx context.Context, httpReq *http.Request, secrets map[string]string) error +} + +// Builder constructs a Signer from an AuthConfig. A single Builder instance +// may be reused across many requests; it owns shared state such as the OAuth2 +// token cache. +type Builder struct { + httpClient *http.Client + oauthCache *oauth2Cache +} + +// NewBuilder returns a Builder configured with the supplied http.Client. +// The client is used by OAuth2 signers to reach the IdP's token endpoint. +// If nil, http.DefaultClient is used. +func NewBuilder(client *http.Client) *Builder { + if client == nil { + client = http.DefaultClient + } + return &Builder{ + httpClient: client, + oauthCache: newOAuth2Cache(), + } +} + +// Build selects the appropriate Signer for the given AuthConfig. +// Returns (nil, nil) when auth is nil — callers should treat that as "no +// signing, send the request as-is". +func (b *Builder) Build(auth *confhttppb.AuthConfig) (Signer, error) { + if auth == nil { + return nil, nil + } + switch m := auth.GetMethod().(type) { + case *confhttppb.AuthConfig_ApiKey: + return newAPIKeySigner(m.ApiKey) + case *confhttppb.AuthConfig_Basic: + return newBasicSigner(m.Basic) + case *confhttppb.AuthConfig_Bearer: + return newBearerSigner(m.Bearer) + case *confhttppb.AuthConfig_Hmac: + return newHmacSigner(m.Hmac) + case *confhttppb.AuthConfig_Oauth2: + return newOAuth2Signer(m.Oauth2, b.httpClient, b.oauthCache) + case nil: + return nil, errors.New("auth method not set") + default: + return nil, fmt.Errorf("unsupported auth method %T", m) + } +} + +// resolveSecret returns the utf8 secret value for name or an error if the +// workflow did not provide it. The confidentialhttp capability validator +// enforces that every name referenced by AuthConfig appears in +// vault_don_secrets, so missing values here should be treated as an +// internal error in most cases. +func resolveSecret(secrets map[string]string, name string) (string, error) { + if name == "" { + return "", ErrSecretNameEmpty + } + v, ok := secrets[name] + if !ok { + return "", fmt.Errorf("%w: %q", ErrSecretNotProvided, name) + } + if v == "" { + return "", fmt.Errorf("%w: %q", ErrSecretEmpty, name) + } + return v, nil +} diff --git a/pkg/capabilities/v2/actions/confidentialhttp/signer/signer_test.go b/pkg/capabilities/v2/actions/confidentialhttp/signer/signer_test.go new file mode 100644 index 000000000..ca77aaeca --- /dev/null +++ b/pkg/capabilities/v2/actions/confidentialhttp/signer/signer_test.go @@ -0,0 +1,484 @@ +package signer + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" + "time" + + confhttppb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" +) + +// mustReq builds a request with a rewindable body (so HMAC/SigV4 can read +// and restore it). +func mustReq(t *testing.T, method, urlStr, body string) *http.Request { + t.Helper() + var r *http.Request + var err error + if body == "" { + r, err = http.NewRequest(method, urlStr, nil) + } else { + r, err = http.NewRequest(method, urlStr, strings.NewReader(body)) + } + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + return r +} + +func TestBuilder_NilAuth_ReturnsNilSigner(t *testing.T) { + b := NewBuilder(nil) + s, err := b.Build(nil) + if err != nil { + t.Fatalf("Build(nil) err=%v", err) + } + if s != nil { + t.Fatalf("Build(nil) signer=%v, want nil", s) + } +} + +func TestAPIKeySigner(t *testing.T) { + cfg := &confhttppb.ApiKeyAuth{ + HeaderName: "x-api-key", + SecretName: "cg", + ValuePrefix: "", + } + s, err := newAPIKeySigner(cfg) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://example.com/x", "") + if err := s.Sign(context.Background(), req, map[string]string{"cg": "secret123"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if got := req.Header.Get("x-api-key"); got != "secret123" { + t.Fatalf("header got=%q", got) + } +} + +func TestAPIKeySigner_ValuePrefix(t *testing.T) { + s, err := newAPIKeySigner(&confhttppb.ApiKeyAuth{ + HeaderName: "Authorization", SecretName: "tok", ValuePrefix: "ApiKey ", + }) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://example.com/x", "") + if err := s.Sign(context.Background(), req, map[string]string{"tok": "zzz"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if got := req.Header.Get("Authorization"); got != "ApiKey zzz" { + t.Fatalf("got=%q", got) + } +} + +func TestAPIKeySigner_MissingSecret(t *testing.T) { + s, _ := newAPIKeySigner(&confhttppb.ApiKeyAuth{HeaderName: "x", SecretName: "absent"}) + req := mustReq(t, "GET", "https://example.com/x", "") + err := s.Sign(context.Background(), req, map[string]string{}) + if !errors.Is(err, ErrSecretNotProvided) { + t.Fatalf("want ErrSecretNotProvided, got %v", err) + } +} + +func TestBasicSigner(t *testing.T) { + s, err := newBasicSigner(&confhttppb.BasicAuth{ + UsernameSecretName: "u", PasswordSecretName: "p", + }) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://example.com/x", "") + if err := s.Sign(context.Background(), req, map[string]string{"u": "alice", "p": "hunter2"}); err != nil { + t.Fatalf("Sign: %v", err) + } + want := "Basic " + base64.StdEncoding.EncodeToString([]byte("alice:hunter2")) + if got := req.Header.Get("Authorization"); got != want { + t.Fatalf("got=%q want=%q", got, want) + } +} + +func TestBearerSigner_Default(t *testing.T) { + s, err := newBearerSigner(&confhttppb.BearerAuth{TokenSecretName: "t"}) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://example.com/x", "") + _ = s.Sign(context.Background(), req, map[string]string{"t": "abc"}) + if got := req.Header.Get("Authorization"); got != "Bearer abc" { + t.Fatalf("got=%q", got) + } +} + +func TestBearerSigner_CustomHeaderAndPrefix(t *testing.T) { + s, err := newBearerSigner(&confhttppb.BearerAuth{ + TokenSecretName: "t", HeaderName: "Authorization", ValuePrefix: "token ", + }) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://api.github.com/x", "") + _ = s.Sign(context.Background(), req, map[string]string{"t": "gho_xx"}) + if got := req.Header.Get("Authorization"); got != "token gho_xx" { + t.Fatalf("got=%q", got) + } +} + +func TestHmacSha256_BasicCanonical(t *testing.T) { + s, err := newHmacSha256Signer(&confhttppb.HmacSha256{SecretName: "hmac"}) + if err != nil { + t.Fatalf("new: %v", err) + } + // Pin time for determinism. + hs := s.(*hmacSha256Signer) + hs.nowFn = func() time.Time { return time.Unix(1700000000, 0) } + + req := mustReq(t, "POST", "https://example.com/api/v1?x=1", `{"a":1}`) + if err := s.Sign(context.Background(), req, map[string]string{"hmac": "key"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if got := req.Header.Get("X-Timestamp"); got != "1700000000" { + t.Fatalf("timestamp got=%q", got) + } + if req.Header.Get("X-Signature") == "" { + t.Fatalf("signature missing") + } + // Assert body was rewound (canonical call should leave body readable). + if req.Body == nil { + t.Fatalf("body nil after sign") + } + b, _ := io.ReadAll(req.Body) + if string(b) != `{"a":1}` { + t.Fatalf("body after sign=%q", string(b)) + } +} + +func TestHmacCustom_Template(t *testing.T) { + cfg := &confhttppb.HmacCustom{ + SecretName: "k", + CanonicalTemplate: `{{.method}} {{.path}} {{.body_sha256}}`, + Hash: confhttppb.HmacCustom_HASH_SHA256, + SignatureHeader: "X-Sig", + Encoding: "hex", + } + s, err := newHmacCustomSigner(cfg) + if err != nil { + t.Fatalf("new: %v", err) + } + // Pin time + nonce for determinism (not used in template but exercised + // by the nonce path if configured). + hc := s.(*hmacCustomSigner) + hc.nowFn = func() time.Time { return time.Unix(1700000000, 0) } + + req := mustReq(t, "POST", "https://example.com/widgets", `hello`) + if err := s.Sign(context.Background(), req, map[string]string{"k": "k1"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if req.Header.Get("X-Sig") == "" { + t.Fatalf("missing signature") + } +} + +func TestHmacCustom_BadTemplateRejectedAtNew(t *testing.T) { + _, err := newHmacCustomSigner(&confhttppb.HmacCustom{ + SecretName: "k", + CanonicalTemplate: `{{ oopsNoSuchFunc }}`, + Hash: confhttppb.HmacCustom_HASH_SHA256, + SignatureHeader: "X-Sig", + }) + if !errors.Is(err, ErrHmacTemplateParse) { + t.Fatalf("want parse err, got %v", err) + } +} + +func TestHmacCustom_SignatureWithPrefix(t *testing.T) { + s, err := newHmacCustomSigner(&confhttppb.HmacCustom{ + SecretName: "k", + CanonicalTemplate: `{{.method}}`, + Hash: confhttppb.HmacCustom_HASH_SHA512, + SignatureHeader: "X-Vendor-Signature", + SignaturePrefix: "HMAC-SHA512 ", + Encoding: "base64", + }) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "POST", "https://example.com/x", "") + _ = s.Sign(context.Background(), req, map[string]string{"k": "k1"}) + if !strings.HasPrefix(req.Header.Get("X-Vendor-Signature"), "HMAC-SHA512 ") { + t.Fatalf("prefix missing, got=%q", req.Header.Get("X-Vendor-Signature")) + } +} + +// --- AWS SigV4 --- + +func TestAwsSigV4_AttachesExpectedHeaders(t *testing.T) { + s, err := newAwsSigV4Signer(&confhttppb.AwsSigV4{ + AccessKeyIdSecretName: "ak", + SecretAccessKeySecretName: "sk", + Region: "us-east-1", + Service: "execute-api", + }) + if err != nil { + t.Fatalf("new: %v", err) + } + aw := s.(*awsSigV4Signer) + aw.nowFn = func() time.Time { return time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) } + + req := mustReq(t, "POST", "https://api.execute-api.us-east-1.amazonaws.com/prod/x", `{"a":1}`) + if err := s.Sign(context.Background(), req, + map[string]string{"ak": "AKIDEXAMPLE", "sk": "secret"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if req.Header.Get("Authorization") == "" { + t.Fatalf("missing Authorization header") + } + if !strings.Contains(req.Header.Get("Authorization"), "AWS4-HMAC-SHA256") { + t.Fatalf("unexpected auth: %q", req.Header.Get("Authorization")) + } + if req.Header.Get("X-Amz-Date") == "" { + t.Fatalf("missing X-Amz-Date") + } +} + +func TestAwsSigV4_SessionTokenInjected(t *testing.T) { + s, err := newAwsSigV4Signer(&confhttppb.AwsSigV4{ + AccessKeyIdSecretName: "ak", + SecretAccessKeySecretName: "sk", + SessionTokenSecretName: "st", + Region: "us-east-1", + Service: "execute-api", + }) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://api.execute-api.us-east-1.amazonaws.com/prod/x", "") + _ = s.Sign(context.Background(), req, + map[string]string{"ak": "AKID", "sk": "sk", "st": "session"}) + if req.Header.Get("X-Amz-Security-Token") != "session" { + t.Fatalf("missing X-Amz-Security-Token") + } +} + +func TestAwsSigV4_UnsignedPayload(t *testing.T) { + s, err := newAwsSigV4Signer(&confhttppb.AwsSigV4{ + AccessKeyIdSecretName: "ak", + SecretAccessKeySecretName: "sk", + Region: "us-east-1", + Service: "s3", + UnsignedPayload: true, + }) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "PUT", "https://bucket.s3.us-east-1.amazonaws.com/k", "huge-body-bytes") + if err := s.Sign(context.Background(), req, map[string]string{"ak": "AKID", "sk": "sk"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if req.Header.Get("X-Amz-Content-Sha256") != "UNSIGNED-PAYLOAD" { + t.Fatalf("X-Amz-Content-Sha256=%q, want UNSIGNED-PAYLOAD", req.Header.Get("X-Amz-Content-Sha256")) + } +} + +// --- OAuth2 --- + +type idpHandler struct { + hits atomic.Int32 + status int + expires int64 + token string +} + +func (h *idpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.hits.Add(1) + w.Header().Set("Content-Type", "application/json") + if h.status != 0 && h.status != 200 { + w.WriteHeader(h.status) + _, _ = io.WriteString(w, `{"error":"invalid_client"}`) + return + } + tok := h.token + if tok == "" { + tok = "access_token_1" + } + resp := map[string]any{"access_token": tok, "token_type": "Bearer", "expires_in": h.expires} + _ = json.NewEncoder(w).Encode(resp) +} + +// idpFromHandler returns a test server using https via httptest.NewTLSServer +// so the signer's https-only check is satisfied. +func idpFromHandler(t *testing.T, h http.Handler) (*httptest.Server, *http.Client) { + t.Helper() + srv := httptest.NewTLSServer(h) + t.Cleanup(srv.Close) + return srv, srv.Client() +} + +func TestOAuth2ClientCreds_CacheAndSingleFlight(t *testing.T) { + h := &idpHandler{expires: 3600} + srv, client := idpFromHandler(t, h) + + cache := newOAuth2Cache() + s, err := newOAuth2ClientCredsSigner(&confhttppb.OAuth2ClientCredentials{ + TokenUrl: srv.URL, + ClientIdSecretName: "cid", + ClientSecretSecretName: "csec", + Scopes: []string{"read", "write"}, + }, client, cache) + if err != nil { + t.Fatalf("new: %v", err) + } + + secrets := map[string]string{"cid": "id1", "csec": "sec1"} + // Two sequential calls should trigger only ONE IdP hit thanks to cache. + for i := 0; i < 2; i++ { + req := mustReq(t, "GET", "https://api.example.com/x", "") + if err := s.Sign(context.Background(), req, secrets); err != nil { + t.Fatalf("Sign %d: %v", i, err) + } + if !strings.HasPrefix(req.Header.Get("Authorization"), "Bearer ") { + t.Fatalf("missing Bearer header, got %q", req.Header.Get("Authorization")) + } + } + if got := h.hits.Load(); got != 1 { + t.Fatalf("idp hits=%d, want 1 (cache miss should happen once)", got) + } +} + +func TestOAuth2ClientCreds_IdPFailure_SurfacesTypedError(t *testing.T) { + h := &idpHandler{status: 401} + srv, client := idpFromHandler(t, h) + cache := newOAuth2Cache() + s, err := newOAuth2ClientCredsSigner(&confhttppb.OAuth2ClientCredentials{ + TokenUrl: srv.URL, + ClientIdSecretName: "cid", + ClientSecretSecretName: "csec", + }, client, cache) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://api.example.com/x", "") + err = s.Sign(context.Background(), req, map[string]string{"cid": "x", "csec": "y"}) + if !errors.Is(err, ErrOAuth2TokenEndpointHTTPError) { + t.Fatalf("want ErrOAuth2TokenEndpointHTTPError, got %v", err) + } +} + +func TestOAuth2ClientCreds_NonHTTPSRejected(t *testing.T) { + cache := newOAuth2Cache() + s, err := newOAuth2ClientCredsSigner(&confhttppb.OAuth2ClientCredentials{ + TokenUrl: "http://insecure.example.com/token", + ClientIdSecretName: "cid", + ClientSecretSecretName: "csec", + }, http.DefaultClient, cache) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://api.example.com/x", "") + err = s.Sign(context.Background(), req, map[string]string{"cid": "x", "csec": "y"}) + if !errors.Is(err, ErrOAuth2TokenURLInvalid) { + t.Fatalf("want ErrOAuth2TokenURLInvalid, got %v", err) + } +} + +func TestOAuth2ClientCreds_CacheMiss_OnExpiry(t *testing.T) { + h := &idpHandler{expires: 1} // 1s expires_in + 60s safety → instant miss + srv, client := idpFromHandler(t, h) + + cache := newOAuth2Cache() + // Force expiration to be immediate by advancing the clock inside put. + cache.nowFn = func() time.Time { return time.Now() } + + s, err := newOAuth2ClientCredsSigner(&confhttppb.OAuth2ClientCredentials{ + TokenUrl: srv.URL, + ClientIdSecretName: "cid", + ClientSecretSecretName: "csec", + }, client, cache) + if err != nil { + t.Fatalf("new: %v", err) + } + secrets := map[string]string{"cid": "id1", "csec": "sec1"} + + req1 := mustReq(t, "GET", "https://api.example.com/x", "") + if err := s.Sign(context.Background(), req1, secrets); err != nil { + t.Fatalf("Sign #1: %v", err) + } + // expires_in=1 -> safety=60s -> already expired. Second call must + // re-fetch. + req2 := mustReq(t, "GET", "https://api.example.com/x", "") + if err := s.Sign(context.Background(), req2, secrets); err != nil { + t.Fatalf("Sign #2: %v", err) + } + if got := h.hits.Load(); got < 2 { + t.Fatalf("expected >=2 idp hits after expiry, got %d", got) + } +} + +func TestOAuth2RefreshToken_UsesRefreshGrant(t *testing.T) { + var formSeen url.Values + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = r.ParseForm() + formSeen = r.PostForm + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "access_after_refresh", "expires_in": 3600, + }) + }) + srv, client := idpFromHandler(t, h) + cache := newOAuth2Cache() + + s, err := newOAuth2RefreshSigner(&confhttppb.OAuth2RefreshToken{ + TokenUrl: srv.URL, + RefreshTokenSecretName: "rt", + ClientIdSecretName: "cid", + ClientSecretSecretName: "csec", + }, client, cache) + if err != nil { + t.Fatalf("new: %v", err) + } + req := mustReq(t, "GET", "https://api.example.com/x", "") + if err := s.Sign(context.Background(), req, + map[string]string{"rt": "my_refresh", "cid": "id1", "csec": "sec1"}); err != nil { + t.Fatalf("Sign: %v", err) + } + if req.Header.Get("Authorization") != "Bearer access_after_refresh" { + t.Fatalf("unexpected bearer: %q", req.Header.Get("Authorization")) + } + if formSeen.Get("grant_type") != "refresh_token" { + t.Fatalf("grant_type=%q", formSeen.Get("grant_type")) + } + if formSeen.Get("refresh_token") != "my_refresh" { + t.Fatalf("refresh_token form value missing") + } +} + +func TestOAuth2RefreshToken_CacheKeyDependsOnRefreshFingerprint(t *testing.T) { + h := &idpHandler{expires: 3600, token: "t1"} + srv, client := idpFromHandler(t, h) + cache := newOAuth2Cache() + s, err := newOAuth2RefreshSigner(&confhttppb.OAuth2RefreshToken{ + TokenUrl: srv.URL, + RefreshTokenSecretName: "rt", + }, client, cache) + if err != nil { + t.Fatalf("new: %v", err) + } + + req := mustReq(t, "GET", "https://api.example.com/x", "") + _ = s.Sign(context.Background(), req, map[string]string{"rt": "refresh_A"}) + // Call with a DIFFERENT refresh token; cache must miss. + req2 := mustReq(t, "GET", "https://api.example.com/x", "") + _ = s.Sign(context.Background(), req2, map[string]string{"rt": "refresh_B"}) + + if h.hits.Load() != 2 { + t.Fatalf("expected 2 idp hits (different refresh token fingerprints), got %d", h.hits.Load()) + } +}