Skip to content

feat: support key rotation #2099

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 98 additions & 84 deletions app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ message Data {
message Auth {
// Authentication creates a JWT that uses this secret for signing
string generated_jws_hmac_secret = 2;
// Additional HMAC verification keys for the JWT
// useful for key rotation
repeated string additional_hmac_verification_keys = 8;
AllowList allow_list = 3;
string cas_robot_account_private_key_path = 4;
OIDC oidc = 6;
Expand Down
46 changes: 23 additions & 23 deletions app/controlplane/internal/server/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,20 @@ import (
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/sentrycontext"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/multijwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
authzMiddleware "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz/middleware"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/user"

"github.com/bufbuild/protovalidate-go"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/service"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext"
"github.com/chainloop-dev/chainloop/pkg/credentials"
"github.com/getsentry/sentry-go"
"github.com/golang-jwt/jwt/v4"

"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
"github.com/go-kratos/kratos/v2/middleware/logging"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/middleware/selector"
Expand Down Expand Up @@ -170,17 +167,28 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {

logHelper := log.NewHelper(opts.Logger)

// List of providers to verify the JWT
// We support user and API token verification
jwtVerificationProviders := []multijwtmiddleware.JWTOption{
multijwtmiddleware.NewUserTokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
multijwtmiddleware.NewAPITokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
}

// any additional verification keys will be used to verify the user token
for _, key := range opts.AuthConfig.AdditionalHmacVerificationKeys {
jwtVerificationProviders = append(jwtVerificationProviders, multijwtmiddleware.NewUserTokenProvider(key), multijwtmiddleware.NewAPITokenProvider(key))
}

// User authentication
middlewares = append(middlewares,
usercontext.Prometheus(),
// If we require a logged in user we
selector.Server(
// 1 - Extract the currentUser/API token from the JWT
// NOTE: this works because both currentUser and API tokens JWT use the same signing method and secret
jwtMiddleware.Server(func(_ *jwt.Token) (interface{}, error) {
return []byte(opts.AuthConfig.GeneratedJwsHmacSecret), nil
},
jwtMiddleware.WithSigningMethod(user.SigningMethod),
multijwtmiddleware.WithJWTMulti(
opts.Logger,
jwtVerificationProviders...,
),
// 2.a - Set its API token and organization as alternative to the user
usercontext.WithCurrentAPITokenAndOrgMiddleware(opts.APITokenUseCase, opts.OrganizationUseCase, logHelper),
Expand All @@ -207,24 +215,16 @@ func craftMiddleware(opts *Opts) []middleware.Middleware {
// if we require a robot account
selector.Server(
// 1 - Extract information from the JWT by using the claims
attjwtmiddleware.WithJWTMulti(
multijwtmiddleware.WithJWTMulti(
opts.Logger,
// Robot account provider
attjwtmiddleware.NewRobotAccountProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
// API Token provider
attjwtmiddleware.NewAPITokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
// User Token provider
attjwtmiddleware.NewUserTokenProvider(opts.AuthConfig.GeneratedJwsHmacSecret),
// Delegated Federated provider
attjwtmiddleware.WithFederatedProvider(opts.FederatedConfig),
// It also supports federated delegation
append(jwtVerificationProviders, multijwtmiddleware.WithFederatedProvider(opts.FederatedConfig))...,
),
// 2.a - Set its workflow and organization in the context
usercontext.WithAttestationContextFromRobotAccount(opts.RobotAccountUseCase, opts.OrganizationUseCase, logHelper),
// 2.b - Set its API token and Robot Account as alternative to the user
usercontext.WithAttestationContextFromAPIToken(opts.APITokenUseCase, opts.OrganizationUseCase, logHelper),
// 2.c - Set Attestation context from user token
// 2.a - Set its API token and Robot Account as alternative to the user
usercontext.WithCurrentAPITokenAndOrgMiddleware(opts.APITokenUseCase, opts.OrganizationUseCase, logHelper),
// 2.b - Set Attestation context from user token
usercontext.WithAttestationContextFromUser(opts.UserUseCase, logHelper),
// 2.d - Set its robot account from federated delegation
// 2.c - Set its robot account from federated delegation
usercontext.WithAttestationContextFromFederatedInfo(opts.OrganizationUseCase, logHelper),
).Match(requireRobotAccountMatcher()).Build(),
)
Expand Down
24 changes: 12 additions & 12 deletions app/controlplane/internal/service/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/dispatcher"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/multijwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
"github.com/chainloop-dev/chainloop/pkg/attestation"
Expand Down Expand Up @@ -98,7 +98,7 @@ func NewAttestationService(opts *NewAttestationServiceOpts) *AttestationService
}

func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.AttestationServiceGetContractRequest) (*cpAPI.AttestationServiceGetContractResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "neither robot account nor API token found")
}
Expand Down Expand Up @@ -129,7 +129,7 @@ func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.Attesta
}

func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationServiceInitRequest) (*cpAPI.AttestationServiceInitResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "neither robot account nor API token found")
}
Expand Down Expand Up @@ -201,7 +201,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer
}

func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationServiceStoreRequest) (*cpAPI.AttestationServiceStoreResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand Down Expand Up @@ -243,7 +243,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe
}

// Stores and process a DSSE Envelope with a Chainloop attestation
func (s *AttestationService) storeAttestation(ctx context.Context, envelope []byte, bundle []byte, robotAccount *usercontext.RobotAccount, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) {
func (s *AttestationService) storeAttestation(ctx context.Context, envelope []byte, bundle []byte, robotAccount *usercontext.AttAuth, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) {
workflowRunID := wfRun.ID.String()
casBackend := wfRun.CASBackends[0]

Expand Down Expand Up @@ -341,7 +341,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope []by
}

func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationServiceCancelRequest) (*cpAPI.AttestationServiceCancelResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand Down Expand Up @@ -381,7 +381,7 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS
// There is another endpoint to get credentials via casCredentialsService.Get
// This one is kept since it leverages robot-accounts in the context of a workflow
func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.AttestationServiceGetUploadCredsRequest) (*cpAPI.AttestationServiceGetUploadCredsResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand Down Expand Up @@ -419,7 +419,7 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte
}

func (s *AttestationService) GetPolicy(ctx context.Context, req *cpAPI.AttestationServiceGetPolicyRequest) (*cpAPI.AttestationServiceGetPolicyResponse, error) {
token, ok := attjwtmiddleware.FromJWTAuthContext(ctx)
token, ok := multijwtmiddleware.FromJWTAuthContext(ctx)
if !ok {
return nil, errors.Forbidden("forbidden", "token not found")
}
Expand All @@ -441,7 +441,7 @@ func (s *AttestationService) GetPolicy(ctx context.Context, req *cpAPI.Attestati
}

func (s *AttestationService) GetPolicyGroup(ctx context.Context, req *cpAPI.AttestationServiceGetPolicyGroupRequest) (*cpAPI.AttestationServiceGetPolicyGroupResponse, error) {
token, ok := attjwtmiddleware.FromJWTAuthContext(ctx)
token, ok := multijwtmiddleware.FromJWTAuthContext(ctx)
if !ok {
return nil, errors.Forbidden("forbidden", "token not found")
}
Expand Down Expand Up @@ -623,21 +623,21 @@ func (s *AttestationService) findWorkflowFromTokenOrNameOrRunID(ctx context.Cont
return nil, biz.NewErrValidationStr("workflowName or workflowRunId must be provided")
}

func checkAuthRequirements(attToken *usercontext.RobotAccount, workflowName string) error {
func checkAuthRequirements(attToken *usercontext.AttAuth, workflowName string) error {
if attToken == nil {
return errors.Forbidden("forbidden", "authentication not found")
}

// For API tokens we do not support explicit workflowName. It is inside the token
if attToken.ProviderKey == attjwtmiddleware.APITokenProviderKey && workflowName == "" {
if attToken.ProviderKey == multijwtmiddleware.APITokenProviderKey && workflowName == "" {
return errors.BadRequest("bad request", "when using an API Token, workflow name is required as parameter")
}

return nil
}

func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAPI.FindOrCreateWorkflowRequest) (*cpAPI.FindOrCreateWorkflowResponse, error) {
apiToken := usercontext.CurrentRobotAccount(ctx)
apiToken := usercontext.CurrentAttestationAuth(ctx)
if apiToken == nil {
return nil, errors.NotFound("not found", "neither robot account nor API token found")
}
Expand Down
14 changes: 7 additions & 7 deletions app/controlplane/internal/service/attestationstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/multijwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"

errors "github.com/go-kratos/kratos/v2/errors"
Expand Down Expand Up @@ -57,7 +57,7 @@ func NewAttestationStateService(opts *NewAttestationStateServiceOpt) *Attestatio
}

func (s *AttestationStateService) Initialized(ctx context.Context, req *cpAPI.AttestationStateServiceInitializedRequest) (*cpAPI.AttestationStateServiceInitializedResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand All @@ -79,7 +79,7 @@ func (s *AttestationStateService) Initialized(ctx context.Context, req *cpAPI.At
}

func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.AttestationStateServiceSaveRequest) (*cpAPI.AttestationStateServiceSaveResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand Down Expand Up @@ -107,7 +107,7 @@ func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.Attestati
}

func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.AttestationStateServiceReadRequest) (*cpAPI.AttestationStateServiceReadResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand Down Expand Up @@ -136,7 +136,7 @@ func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.Attestati
}

func (s *AttestationStateService) Reset(ctx context.Context, req *cpAPI.AttestationStateServiceResetRequest) (*cpAPI.AttestationStateServiceResetResponse, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return nil, errors.NotFound("not found", "robot account not found")
}
Expand All @@ -159,12 +159,12 @@ func (s *AttestationStateService) Reset(ctx context.Context, req *cpAPI.Attestat
// NOTE: Using the robot-account as JWT is not ideal but it's a start
// TODO: look into using some identifier from the actual client like machine-uuid
func encryptionPassphrase(ctx context.Context) (string, error) {
robotAccount := usercontext.CurrentRobotAccount(ctx)
robotAccount := usercontext.CurrentAttestationAuth(ctx)
if robotAccount == nil {
return "", errors.NotFound("not found", "robot account not found")
// If we are using a federated provider, we'll use the provider key as the passphrase since we can not guarantee the stability of the token
// In practice this means disabling the state encryption at rest but this state in practice is a subset of the resulting attestation that we end up storing
} else if robotAccount.ProviderKey == attjwtmiddleware.FederatedProviderKey {
} else if robotAccount.ProviderKey == multijwtmiddleware.FederatedProviderKey {
return robotAccount.ProviderKey, nil
}

Expand Down
6 changes: 3 additions & 3 deletions app/controlplane/internal/service/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import (
"fmt"
"net/http"

"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/multijwtmiddleware"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/apitoken"

jwtmiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
"github.com/gorilla/mux"
"github.com/prometheus/common/expfmt"
)
Expand Down Expand Up @@ -60,13 +60,13 @@ func (p *PrometheusService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

// Extracts the organization name from the request
rawClaims, ok := jwtmiddleware.FromContext(r.Context())
rawClaims, ok := multijwtmiddleware.FromJWTAuthContext(r.Context())
if !ok {
http.Error(w, "Error extracting claims from context", http.StatusInternalServerError)
return
}

apiTokenClaims, ok := rawClaims.(*apitoken.CustomClaims)
apiTokenClaims, ok := rawClaims.Claims.(*apitoken.CustomClaims)
if !ok {
http.Error(w, "Error extracting API Token claims", http.StatusInternalServerError)
return
Expand Down
2 changes: 1 addition & 1 deletion app/controlplane/internal/service/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func NewSigningService(signing *biz.SigningUseCase, opts ...NewOpt) *SigningServ
}

func (s *SigningService) GenerateSigningCert(ctx context.Context, req *v1.GenerateSigningCertRequest) (*v1.GenerateSigningCertResponse, error) {
ra := usercontext.CurrentRobotAccount(ctx)
ra := usercontext.CurrentAttestationAuth(ctx)
if ra == nil {
return nil, errors.Unauthorized("missing org", "authentication data is required")
}
Expand Down
Loading
Loading