Skip to content

Commit

Permalink
add context signature to request body in osb provision and bind (#703)
Browse files Browse the repository at this point in the history
add context signature to request body in osb/smaap provision/update-instance and bind
  • Loading branch information
nirbenrey authored Oct 20, 2021
1 parent 06d970e commit 8d681b0
Show file tree
Hide file tree
Showing 10 changed files with 566 additions and 8 deletions.
3 changes: 3 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ type Settings struct {
RateLimitExcludePaths []string `mapstructure:"rate_limit_exclude_paths" description:"define paths that should be excluded from the rate limiter processing"`
RateLimitUsageLogThreshold int64 `mapstructure:"rate_limiting_usage_log_threshold" description:"defines a threshold for log notification trigger about requests limit usage. Accepts value in range from 0 to 100 (percents)"`
DisabledQueryParameters []string `mapstructure:"disabled_query_parameters" description:"which query parameters are not implemented by service manager and should be extended"`
OSBRSAPublicKey string `mapstructure:"osb_rsa_public_key"`
OSBRSAPrivateKey string `mapstructure:"osb_rsa_private_key"`
}

// DefaultSettings returns default values for API settings
Expand Down Expand Up @@ -140,6 +142,7 @@ func New(ctx context.Context, e env.Environment, options *Options) (*web.API, er
TokenIssuer: options.APISettings.TokenIssuerURL,
TokenBasicAuth: options.APISettings.TokenBasicAuth,
ServiceManagerTenantId: options.APISettings.ServiceManagerTenantId,
ContextRSAPublicKey: options.APISettings.OSBRSAPublicKey,
},
&osb.Controller{
BrokerFetcher: func(ctx context.Context, brokerID string) (*types.ServiceBroker, error) {
Expand Down
1 change: 1 addition & 0 deletions api/info/info_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Controller struct {
// as basic auth (true) or in the body (false)
TokenBasicAuth bool `json:"token_basic_auth"`
ServiceManagerTenantId string `json:"service_manager_tenant_id"`
ContextRSAPublicKey string `json:"context_rsa_public_key,omitempty"`
}

var _ web.Controller = &Controller{}
Expand Down
149 changes: 149 additions & 0 deletions api/osb/context_signature_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package osb

import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
//"encoding/json"
"encoding/pem"
"fmt"
"github.com/Peripli/service-manager/pkg/log"
"github.com/Peripli/service-manager/pkg/web"
)

const ContextSignaturePluginName = "ContextSignaturePlugin"

type ContextSignaturePlugin struct {
contextSigner *ContextSigner
}

type ContextSigner struct {
ContextPrivateKey string
rsaPrivateKey *rsa.PrivateKey
}

func NewCtxSignaturePlugin(contextSigner *ContextSigner) *ContextSignaturePlugin {
return &ContextSignaturePlugin{
contextSigner: contextSigner,
}
}

func (s *ContextSignaturePlugin) Name() string {
return ContextSignaturePluginName
}

func (s *ContextSignaturePlugin) Provision(req *web.Request, next web.Handler) (*web.Response, error) {
return s.signContext(req, next)
}

func (s *ContextSignaturePlugin) Bind(req *web.Request, next web.Handler) (*web.Response, error) {
return s.signContext(req, next)
}

func (s *ContextSignaturePlugin) UpdateService(req *web.Request, next web.Handler) (*web.Response, error) {
return s.signContext(req, next)
}

func (s *ContextSignaturePlugin) signContext(req *web.Request, next web.Handler) (*web.Response, error) {
//in case the private key is not provided we continue without adding the signature. this is useful in case we want to toggle off the feature
if s.contextSigner.ContextPrivateKey == "" {
log.C(req.Context()).Debugf("context private key not found. context signature can not be calculated")
return next.Handle(req)
}
//unmarshal and marshal the request body so the fields within the context will be ordered lexicographically, and to get rid of redundant spaces\drop-line\tabs
var reqBodyMap map[string]interface{}
err := json.Unmarshal(req.Body, &reqBodyMap)
if err != nil {
log.C(req.Context()).Errorf("failed to unmarshal context: %v", err)
return nil, err
}
if _, found := reqBodyMap["context"]; !found {
errorMsg := "context not found on request body"
log.C(req.Context()).Error(errorMsg)
return nil, fmt.Errorf(errorMsg)
}
contextMap := reqBodyMap["context"].(map[string]interface{})

err = s.contextSigner.Sign(req.Context(), contextMap)
if err != nil {
log.C(req.Context()).Errorf("failed to sign request context: %v", err)
return nil, err
}

reqBody, err := json.Marshal(reqBodyMap)
if err != nil {
log.C(req.Context()).Errorf("failed to marshal request body: %v", err)
return nil, err
}
req.Body = reqBody

return next.Handle(req)
}

func (cs *ContextSigner) Sign(ctx context.Context, contextMap map[string]interface{}) error {
if cs.ContextPrivateKey == "" {
errorMsg := "context rsa private key is missing. context signature can not be calculated"
log.C(ctx).Errorf(errorMsg)
return fmt.Errorf(errorMsg)
}
ctxByte, err := json.Marshal(contextMap)
if err != nil {
log.C(ctx).Errorf("failed to marshal context: %v", err)
return err
}

//on the first time the sign function is executed we should parse the rsa private key and keep it for next executions
if cs.rsaPrivateKey == nil {
cs.rsaPrivateKey, err = cs.parseRsaPrivateKey(ctx, cs.ContextPrivateKey)
if err != nil {
log.C(ctx).Errorf("failed to parse rsa private key: %v", err)
return err
}
}
signedCtx, err := cs.calculateSignature(ctx, string(ctxByte), cs.rsaPrivateKey)
if err != nil {
log.C(ctx).Errorf("failed to calculate the context signature: %v", err)
return err
}

contextMap["signature"] = signedCtx
return nil
}

func (cs *ContextSigner) parseRsaPrivateKey(ctx context.Context, rsaPrivateKey string) (*rsa.PrivateKey, error) {
key, err := base64.StdEncoding.DecodeString(rsaPrivateKey)
if err != nil {
log.C(ctx).Errorf("failed to base64 decode rsa private key: %v", err)
return nil, err
}
block, _ := pem.Decode(key)
if block == nil {
log.C(ctx).Error("failed to pem decode rsa private key")
return nil, fmt.Errorf("failed to pem decode context rsa private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
log.C(ctx).Errorf("fail to parse rsa key, %s", err.Error())
return nil, err
}

return privateKey, nil
}

func (cs *ContextSigner) calculateSignature(ctx context.Context, ctxStr string, rsaPrivateKey *rsa.PrivateKey) (string, error) {
log.C(ctx).Debugf("creating signature for ctx: %s", ctxStr)

hashedCtx := sha256.Sum256([]byte(ctxStr))

signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashedCtx[:])
if err != nil {
log.C(ctx).Errorf("failed to encrypt context %v", err)
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
27 changes: 25 additions & 2 deletions api/osb/osb_store_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,12 @@ func (sp *storePlugin) storeInstance(ctx context.Context, storage storage.Reposi
}

referencedInstanceID := gjson.GetBytes(req.RawParameters, instance_sharing.ReferencedInstanceIDKey).String()

reqCtx, err := DeleteSignatureField(req.RawContext)
if err != nil {
log.C(ctx).Errorf("failed to delete signature from context: %v", err)
return err
}
instance := &types.ServiceInstance{
Base: types.Base{
ID: req.InstanceID,
Expand All @@ -821,7 +827,7 @@ func (sp *storePlugin) storeInstance(ctx context.Context, storage storage.Reposi
PlatformID: req.PlatformID,
DashboardURL: resp.DashboardURL,
MaintenanceInfo: req.RawMaintenanceInfo,
Context: req.RawContext,
Context: reqCtx,
Usable: true,
ReferencedInstanceID: referencedInstanceID,
}
Expand All @@ -837,6 +843,11 @@ func (sp *storePlugin) storeBinding(ctx context.Context, repository storage.Repo
log.C(ctx).Debugf("Binding name missing. Defaulting to id %s", req.BindingID)
bindingName = req.BindingID
}
reqCtx, err := DeleteSignatureField(req.RawContext)
if err != nil {
log.C(ctx).Errorf("failed to delete signature from context: %v", err)
return err
}
binding := &types.ServiceBinding{
Base: types.Base{
ID: req.BindingID,
Expand All @@ -851,7 +862,7 @@ func (sp *storePlugin) storeBinding(ctx context.Context, repository storage.Repo
RouteServiceURL: resp.RouteServiceUrl,
VolumeMounts: resp.VolumeMounts,
Endpoints: resp.Endpoints,
Context: req.RawContext,
Context: reqCtx,
BindResource: req.BindResource,
Parameters: req.Parameters,
Credentials: nil,
Expand Down Expand Up @@ -1045,6 +1056,18 @@ func decodeRequestBody(request *web.Request, body commonOSBRequest) error {
return parseRequestForm(request, body)
}

func DeleteSignatureField(rawContext json.RawMessage) ([]byte, error) {
if rawContext != nil {
updatedContext, err := sjson.DeleteBytes(rawContext, "signature")
if err != nil {
log.D().Errorf("failed to delete signature from context: %v", err)
return nil, err
}
return updatedContext, nil
}
return rawContext, nil
}

func (sp *storePlugin) handlePollDeleteResponse(ctx context.Context, storage storage.Repository, resp brokerError, state types.OperationState, operationFromDB *types.Operation, correlationID string) (entityOperation, error) {
var entOp entityOperation
switch state {
Expand Down
2 changes: 2 additions & 0 deletions pkg/sm/sm.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ func New(ctx context.Context, cancel context.CancelFunc, e env.Environment, cfg
smb.RegisterPlugins(osb.NewCheckPlatformIDPlugin(interceptableRepository))
smb.RegisterPlugins(osb.NewPlatformTerminationPlugin(interceptableRepository))
smb.RegisterPlugins(osb.NewInstanceSharingPlugin(transactionalRepository, cfg.Multitenancy.LabelKey))
smb.RegisterPlugins(osb.NewCtxSignaturePlugin(&osb.ContextSigner{ContextPrivateKey: cfg.API.OSBRSAPrivateKey}))

// Register default interceptors that represent the core SM business logic
smb.
Expand Down Expand Up @@ -252,6 +253,7 @@ func New(ctx context.Context, cancel context.CancelFunc, e env.Environment, cfg
Repository: interceptableRepository,
TenantKey: cfg.Multitenancy.LabelKey,
PollingInterval: cfg.Operations.PollingInterval,
ContextSigner: &osb.ContextSigner{ContextPrivateKey: cfg.API.OSBRSAPrivateKey},
}

smb.
Expand Down
23 changes: 21 additions & 2 deletions storage/interceptors/smaap_service_binding_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/Peripli/service-manager/api/osb"
"math"
"net/http"
"time"
Expand Down Expand Up @@ -59,6 +60,7 @@ func (p *ServiceBindingCreateInterceptorProvider) Provide() storage.CreateAround
repository: p.Repository,
tenantKey: p.TenantKey,
pollingInterval: p.PollingInterval,
contextSigner: p.ContextSigner,
}
}

Expand Down Expand Up @@ -91,6 +93,7 @@ type ServiceBindingInterceptor struct {
repository *storage.InterceptableTransactionalRepository
tenantKey string
pollingInterval time.Duration
contextSigner *osb.ContextSigner
}

func (i *ServiceBindingInterceptor) AroundTxCreate(f storage.InterceptCreateAroundTxFunc) storage.InterceptCreateAroundTxFunc {
Expand Down Expand Up @@ -177,7 +180,7 @@ func (i *ServiceBindingInterceptor) AroundTxCreate(f storage.InterceptCreateArou
var bindResponse *osbc.BindResponse
if !operation.Reschedule {
operation.Context.ServiceInstanceID = binding.ServiceInstanceID
bindRequest, err := i.prepareBindRequest(instance, binding, service.CatalogID, plan.CatalogID, service.BindingsRetrievable, operation.GetUserInfo())
bindRequest, err := i.prepareBindRequest(ctx, instance, binding, service.CatalogID, plan.CatalogID, service.BindingsRetrievable, operation.GetUserInfo())
if err != nil {
return nil, fmt.Errorf("failed to prepare bind request: %s", err)
}
Expand Down Expand Up @@ -253,6 +256,13 @@ func (i *ServiceBindingInterceptor) AroundTxCreate(f storage.InterceptCreateArou
// we revert the binding context using the original instance context before saving in the database
binding.Context = instanceContext
}
// remove signature field from context before persisting it
binding.Context, err = osb.DeleteSignatureField(binding.Context)
if err != nil {
log.C(ctx).Errorf("failed to delete signature from binding ctx: %v", err)
return nil, err
}

object, err := f(ctx, obj)
if err != nil {
return nil, err
Expand Down Expand Up @@ -473,7 +483,7 @@ func getInstanceByID(ctx context.Context, instanceID string, repository storage.
return instanceObject.(*types.ServiceInstance), nil
}

func (i *ServiceBindingInterceptor) prepareBindRequest(instance *types.ServiceInstance, binding *types.ServiceBinding, serviceCatalogID, planCatalogID string, bindingRetrievable bool, userInfo *types.UserInfo) (*osbc.BindRequest, error) {
func (i *ServiceBindingInterceptor) prepareBindRequest(ctx context.Context, instance *types.ServiceInstance, binding *types.ServiceBinding, serviceCatalogID, planCatalogID string, bindingRetrievable bool, userInfo *types.UserInfo) (*osbc.BindRequest, error) {
context := make(map[string]interface{})
if len(binding.Context) != 0 {
var err error
Expand Down Expand Up @@ -504,6 +514,15 @@ func (i *ServiceBindingInterceptor) prepareBindRequest(instance *types.ServiceIn
binding.Context = contextBytes
}

//in case the private key is not provided we continue without adding the signature. this is useful in case we want to toggle off the feature
if i.contextSigner.ContextPrivateKey != "" {
err := i.contextSigner.Sign(ctx, context)
if err != nil {
log.C(ctx).Errorf("failed to sign context: %v", err)
return nil, err
}
}

bindRequest := &osbc.BindRequest{
BindingID: binding.ID,
InstanceID: instance.ID,
Expand Down
Loading

0 comments on commit 8d681b0

Please sign in to comment.