diff --git a/agent/agent_configuration.go b/agent/agent_configuration.go index 3b9b494e19..0655437e57 100644 --- a/agent/agent_configuration.go +++ b/agent/agent_configuration.go @@ -3,8 +3,6 @@ package agent import ( "regexp" "time" - - "github.com/lestrrat-go/jwx/v2/jwk" ) // AgentConfiguration is the run-time configuration for an agent that @@ -39,10 +37,11 @@ type AgentConfiguration struct { SigningJWKSFile string // Where to find the key to sign pipeline uploads with (passed through to jobs, they might be uploading pipelines) SigningJWKSKeyID string // The key ID to sign pipeline uploads with + SigningAWSKMSKey string // The KMS key ID to sign pipeline uploads with DebugSigning bool // Whether to print step payloads when signing them - VerificationJWKS jwk.Set // The set of keys to verify jobs with - VerificationFailureBehaviour string // What to do if job verification fails (one of `block` or `warn`) + VerificationJWKS any // The set of keys to verify jobs with + VerificationFailureBehaviour string // What to do if job verification fails (one of `block` or `warn`) ANSITimestamps bool TimestampLines bool diff --git a/agent/job_runner.go b/agent/job_runner.go index 4881292178..44662bd022 100644 --- a/agent/job_runner.go +++ b/agent/job_runner.go @@ -22,7 +22,6 @@ import ( "github.com/buildkite/agent/v3/status" "github.com/buildkite/roko" "github.com/buildkite/shellwords" - "github.com/lestrrat-go/jwx/v2/jwk" ) const ( @@ -85,7 +84,7 @@ type JobRunnerConfig struct { JobStatusInterval time.Duration // The JSON Web Keyset for verifying the job - JWKS jwk.Set + JWKS any // A scope for metrics within a job MetricsScope *metrics.Scope @@ -524,6 +523,11 @@ func (r *JobRunner) createEnvironment(ctx context.Context) ([]string, error) { env["BUILDKITE_PTY"] = "false" } + // pass through the KMS key ID for signing + if r.conf.AgentConfiguration.SigningAWSKMSKey != "" { + env["BUILDKITE_AGENT_AWS_KMS_KEY"] = r.conf.AgentConfiguration.SigningAWSKMSKey + } + // Pass signing details through to the executor - any pipelines uploaded by this agent will be signed if r.conf.AgentConfiguration.SigningJWKSFile != "" { env["BUILDKITE_AGENT_JWKS_FILE"] = r.conf.AgentConfiguration.SigningJWKSFile diff --git a/agent/verify_job.go b/agent/verify_job.go index aa4757ae10..6a30a88814 100644 --- a/agent/verify_job.go +++ b/agent/verify_job.go @@ -10,7 +10,6 @@ import ( "github.com/buildkite/go-pipeline/signature" "github.com/gowebpki/jcs" - "github.com/lestrrat-go/jwx/v2/jwk" ) var ( @@ -35,7 +34,7 @@ func (e *invalidSignatureError) Unwrap() error { return e.underlying } -func (r *JobRunner) verifyJob(ctx context.Context, keySet jwk.Set) error { +func (r *JobRunner) verifyJob(ctx context.Context, keySet any) error { step := r.conf.Job.Step if step.Signature == nil { diff --git a/clicommand/agent_start.go b/clicommand/agent_start.go index 4ea3b6111b..c432c2a9ba 100644 --- a/clicommand/agent_start.go +++ b/clicommand/agent_start.go @@ -18,10 +18,14 @@ import ( "syscall" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/buildkite/agent/v3/agent" "github.com/buildkite/agent/v3/api" "github.com/buildkite/agent/v3/core" "github.com/buildkite/agent/v3/internal/agentapi" + awssigner "github.com/buildkite/agent/v3/internal/cryptosigner/aws" "github.com/buildkite/agent/v3/internal/experiments" "github.com/buildkite/agent/v3/internal/job/hook" "github.com/buildkite/agent/v3/internal/job/shell" @@ -83,8 +87,10 @@ type AgentStartConfig struct { RedactedVars []string `cli:"redacted-vars" normalize:"list"` CancelSignal string `cli:"cancel-signal"` - SigningJWKSFile string `cli:"signing-jwks-file" normalize:"filepath"` SigningJWKSKeyID string `cli:"signing-jwks-key-id"` + + SigningJWKSFile string `cli:"signing-jwks-file" normalize:"filepath"` + SigningAWSKMSKey string `cli:"signing-aws-kms-key"` DebugSigning bool `cli:"debug-signing"` VerificationJWKSFile string `cli:"verification-jwks-file" normalize:"filepath"` @@ -658,6 +664,11 @@ var AgentStartCommand = cli.Command{ Usage: "The JWKS key ID to use when signing the pipeline. If omitted, and the signing JWKS contains only one key, that key will be used.", EnvVar: "BUILDKITE_AGENT_SIGNING_JWKS_KEY_ID", }, + cli.StringFlag{ + Name: "signing-aws-kms-key", + Usage: "The KMS KMS key ID, or key alias used when signing and verifying the pipeline.", + EnvVar: "BUILDKITE_AGENT_SIGNING_AWS_KMS_KEY", + }, cli.BoolFlag{ Name: "debug-signing", Usage: "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled", @@ -878,8 +889,36 @@ var AgentStartCommand = cli.Command{ defer shutdown() } - var verificationJWKS jwk.Set - if cfg.VerificationJWKSFile != "" { + // if the agent is provided a KMS key ID, it should use the KMS signer, otherwise + // it should load the JWKS from the file + var verificationJWKS any + switch { + case cfg.SigningAWSKMSKey != "": + + var logMode aws.ClientLogMode + // log requests and retries if we are debugging signing + // see https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/logging/ + if cfg.DebugSigning { + logMode = aws.LogRetries | aws.LogRequest + } + + // this is currently loaded here to ensure it is ONLY loaded if the agent is using KMS for signing + // this will limit the possible impact of this new SDK on the rest of the agent users + awscfg, err := config.LoadDefaultConfig( + ctx, + config.WithClientLogMode(logMode), + ) + if err != nil { + return fmt.Errorf("failed to load AWS config: %w", err) + } + + // assign a crypto signer which uses the KMS key to sign the pipeline + verificationJWKS, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.SigningAWSKMSKey) + if err != nil { + return fmt.Errorf("couldn't create KMS signer: %w", err) + } + + case cfg.VerificationJWKSFile != "": var err error verificationJWKS, err = parseAndValidateJWKS(ctx, "verification", cfg.VerificationJWKSFile) if err != nil { @@ -958,6 +997,7 @@ var AgentStartCommand = cli.Command{ SigningJWKSFile: cfg.SigningJWKSFile, SigningJWKSKeyID: cfg.SigningJWKSKeyID, + SigningAWSKMSKey: cfg.SigningAWSKMSKey, DebugSigning: cfg.DebugSigning, VerificationJWKS: verificationJWKS, diff --git a/clicommand/pipeline_upload.go b/clicommand/pipeline_upload.go index 34763d7edc..7267b3fce9 100644 --- a/clicommand/pipeline_upload.go +++ b/clicommand/pipeline_upload.go @@ -14,9 +14,12 @@ import ( "strings" "time" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/buildkite/agent/v3/agent" "github.com/buildkite/agent/v3/api" "github.com/buildkite/agent/v3/env" + awssigner "github.com/buildkite/agent/v3/internal/cryptosigner/aws" "github.com/buildkite/agent/v3/internal/experiments" "github.com/buildkite/agent/v3/internal/redact" "github.com/buildkite/agent/v3/internal/replacer" @@ -76,9 +79,10 @@ type PipelineUploadConfig struct { RejectSecrets bool `cli:"reject-secrets"` // Used for signing - JWKSFile string `cli:"jwks-file"` - JWKSKeyID string `cli:"jwks-key-id"` - DebugSigning bool `cli:"debug-signing"` + JWKSFile string `cli:"jwks-file"` + JWKSKeyID string `cli:"jwks-key-id"` + SigningAWSKMSKey string `cli:"signing-aws-kms-key"` + DebugSigning bool `cli:"debug-signing"` // Global flags Debug bool `cli:"debug"` @@ -144,6 +148,11 @@ var PipelineUploadCommand = cli.Command{ Usage: "The JWKS key ID to use when signing the pipeline. Required when using a JWKS", EnvVar: "BUILDKITE_AGENT_JWKS_KEY_ID", }, + cli.StringFlag{ + Name: "signing-aws-kms-key", + Usage: "The AWS KMS key identifier which is used to sign pipelines.", + EnvVar: "BUILDKITE_AGENT_AWS_KMS_KEY", + }, cli.BoolFlag{ Name: "debug-signing", Usage: "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled", @@ -275,11 +284,31 @@ var PipelineUploadCommand = cli.Command{ searchForSecrets(l, &cfg, environ, result, src) } - if cfg.JWKSFile != "" { - key, err := jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID) + var ( + key signature.Key + ) + + switch { + case cfg.SigningAWSKMSKey != "": + awscfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return fmt.Errorf("couldn't load AWS config: %w", err) + } + + // assign a crypto signer which uses the KMS key to sign the pipeline + key, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.SigningAWSKMSKey) + if err != nil { + return fmt.Errorf("couldn't create KMS signer: %w", err) + } + + case cfg.JWKSFile != "": + key, err = jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID) if err != nil { return fmt.Errorf("couldn't read the signing key file: %w", err) } + } + + if key != nil { err = signature.SignSteps( ctx, diff --git a/clicommand/tool_sign.go b/clicommand/tool_sign.go index 48894692b8..e675b7487d 100644 --- a/clicommand/tool_sign.go +++ b/clicommand/tool_sign.go @@ -9,7 +9,10 @@ import ( "os" "strings" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/buildkite/agent/v3/internal/bkgql" + awssigner "github.com/buildkite/agent/v3/internal/cryptosigner/aws" "github.com/buildkite/agent/v3/internal/stdin" "github.com/buildkite/agent/v3/logger" "github.com/buildkite/go-pipeline" @@ -17,7 +20,6 @@ import ( "github.com/buildkite/go-pipeline/signature" "github.com/buildkite/go-pipeline/warning" "github.com/buildkite/interpolate" - "github.com/lestrrat-go/jwx/v2/jwk" "github.com/urfave/cli" "gopkg.in/yaml.v3" ) @@ -31,9 +33,14 @@ type ToolSignConfig struct { NoConfirm bool `cli:"no-confirm"` // Used for signing - JWKSFile string `cli:"jwks-file"` - JWKSKeyID string `cli:"jwks-key-id"` - DebugSigning bool `cli:"debug-signing"` + JWKSFile string `cli:"jwks-file"` + JWKSKeyID string `cli:"jwks-key-id"` + + // AWS KMS key used for signing pipelines + AWSKMSKeyID string `cli:"signing-aws-kms-key"` + + // Enable debug logging for pipeline signing, this depends on debug logging also being enabled + DebugSigning bool `cli:"debug-signing"` // Needed for to use GraphQL API OrganizationSlug string `cli:"organization-slug"` @@ -127,6 +134,11 @@ Signing a pipeline from a file: Usage: "The JWKS key ID to use when signing the pipeline. If none is provided and the JWKS file contains only one key, that key will be used.", EnvVar: "BUILDKITE_AGENT_JWKS_KEY_ID", }, + cli.StringFlag{ + Name: "signing-aws-kms-key", + Usage: "The AWS KMS key identifier which is used to sign pipelines.", + EnvVar: "BUILDKITE_AGENT_AWS_KMS_KEY", + }, cli.BoolFlag{ Name: "debug-signing", Usage: "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled", @@ -170,9 +182,31 @@ Signing a pipeline from a file: ctx, cfg, l, _, done := setupLoggerAndConfig[ToolSignConfig](context.Background(), c) defer done() - key, err := jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID) - if err != nil { - return fmt.Errorf("couldn't read the signing key file: %w", err) + var ( + key signature.Key + err error + ) + + switch { + case cfg.AWSKMSKeyID != "": + // load the AWS SDK V2 config + awscfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return fmt.Errorf("couldn't load AWS config: %w", err) + } + + // assign a crypto signer which uses the KMS key to sign the pipeline + key, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.AWSKMSKeyID) + if err != nil { + return fmt.Errorf("couldn't create KMS signer: %w", err) + } + + default: + key, err = jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID) + if err != nil { + return fmt.Errorf("couldn't read the signing key file: %w", err) + } + } sign := signWithGraphQL @@ -209,7 +243,7 @@ func validateNoInterpolations(pipelineString string) error { return nil } -func signOffline(ctx context.Context, c *cli.Context, l logger.Logger, key jwk.Key, cfg *ToolSignConfig) error { +func signOffline(ctx context.Context, c *cli.Context, l logger.Logger, key signature.Key, cfg *ToolSignConfig) error { if cfg.Repository == "" { return ErrUseGraphQL } @@ -289,7 +323,7 @@ func signOffline(ctx context.Context, c *cli.Context, l logger.Logger, key jwk.K return enc.Encode(parsedPipeline) } -func signWithGraphQL(ctx context.Context, c *cli.Context, l logger.Logger, key jwk.Key, cfg *ToolSignConfig) error { +func signWithGraphQL(ctx context.Context, c *cli.Context, l logger.Logger, key signature.Key, cfg *ToolSignConfig) error { orgPipelineSlug := fmt.Sprintf("%s/%s", cfg.OrganizationSlug, cfg.PipelineSlug) debugL := l.WithFields(logger.StringField("orgPipelineSlug", orgPipelineSlug)) diff --git a/go.mod b/go.mod index c96c1720a4..6939393b37 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,12 @@ require ( github.com/DrJosh9000/zzglob v0.3.4 github.com/Khan/genqlient v0.7.0 github.com/aws/aws-sdk-go v1.55.5 + github.com/aws/aws-sdk-go-v2 v1.30.4 + github.com/aws/aws-sdk-go-v2/config v1.27.30 + github.com/aws/aws-sdk-go-v2/service/kms v1.35.5 github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf github.com/buildkite/bintest/v3 v3.3.0 - github.com/buildkite/go-pipeline v0.12.0 + github.com/buildkite/go-pipeline v0.13.0 github.com/buildkite/interpolate v0.1.3 github.com/buildkite/roko v1.2.0 github.com/buildkite/shellwords v0.0.0-20180315084142-c3f497d1e000 @@ -75,6 +78,17 @@ require ( github.com/alexflint/go-arg v1.4.2 // indirect github.com/alexflint/go-scalar v1.0.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.29 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect + github.com/aws/smithy-go v1.20.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect diff --git a/go.sum b/go.sum index 647fa09926..3cafb4395d 100644 --- a/go.sum +++ b/go.sum @@ -56,14 +56,42 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= +github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.30 h1:AQF3/+rOgeJBQP3iI4vojlPib5X6eeOYoa/af7OxAYg= +github.com/aws/aws-sdk-go-v2/config v1.27.30/go.mod h1:yxqvuubha9Vw8stEgNiStO+yZpP68Wm9hLmcm+R/Qk4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.29 h1:CwGsupsXIlAFYuDVHv1nnK0wnxO0wZ/g1L8DSK/xiIw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.29/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c= +github.com/aws/aws-sdk-go-v2/service/kms v1.35.5 h1:XUomV7SiclZl1QuXORdGcfFqHxEHET7rmNGtxTfNB+M= +github.com/aws/aws-sdk-go-v2/service/kms v1.35.5/go.mod h1:A5CS0VRmxxj2YKYLCY08l/Zzbd01m6JZn0WzxgT1OCA= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 h1:OMsEmCyz2i89XwRwPouAJvhj81wINh+4UK+k/0Yo/q8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.5/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf h1:WCnJxXZXx9c8gwz598wvdqmu+YTzB9wx2X1OovK3Le8= github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf/go.mod h1:CeKhh8xSs3WZAc50xABMxu+FlfAAd5PNumo7NfOv7EE= github.com/buildkite/bintest/v3 v3.3.0 h1:RTWcSaJRlOT6t/K311ejPf+0J3LE/QEODzVG3vlLnWo= github.com/buildkite/bintest/v3 v3.3.0/go.mod h1:btqpTsVODiJcb0NMdkkmtMQ6xoFc2W/nY5yy+3I0zcs= -github.com/buildkite/go-pipeline v0.12.0 h1:3P6i71zqtLK9+vvN4yLFJ61QQvwm3ZBvtR7EgSnArVo= -github.com/buildkite/go-pipeline v0.12.0/go.mod h1:qWZlY1gczr6MQp8lwa6Y0ttvFY/OnYxzrNvAglNFB3U= +github.com/buildkite/go-pipeline v0.13.0 h1:Eu04W+468ib2FJVCX9abNubsepEs8RIxWZr4F2i13bI= +github.com/buildkite/go-pipeline v0.13.0/go.mod h1:qWZlY1gczr6MQp8lwa6Y0ttvFY/OnYxzrNvAglNFB3U= github.com/buildkite/interpolate v0.1.3 h1:OFEhqji1rNTRg0u9DsSodg63sjJQEb1uWbENq9fUOBM= github.com/buildkite/interpolate v0.1.3/go.mod h1:UNVe6A+UfiBNKbhAySrBbZFZFxQ+DXr9nWen6WVt/A8= github.com/buildkite/roko v1.2.0 h1:hbNURz//dQqNl6Eo9awjQOVOZwSDJ8VEbBDxSfT9rGQ= diff --git a/internal/cryptosigner/aws/kms.go b/internal/cryptosigner/aws/kms.go new file mode 100644 index 0000000000..b4287412a0 --- /dev/null +++ b/internal/cryptosigner/aws/kms.go @@ -0,0 +1,165 @@ +package awssigner + +import ( + "context" + "crypto" + "crypto/x509" + "fmt" + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + "github.com/lestrrat-go/jwx/v2/jwa" +) + +var ( + ErrInvalidKeyAlgorithm = fmt.Errorf("invalid key algorithm") + ErrInvalidKeyID = fmt.Errorf("invalid key ID") +) + +// KMS is a crypto.Signer that uses an AWS KMS key for signing. +type KMS struct { + alg types.SigningAlgorithmSpec + jwaAlg jwa.KeyAlgorithm + client *kms.Client + kid string +} + +// NewKMS creates a new ECDSA object. This object isnot complete by itself -- it +// needs is setup with the algorithm name to use (see +// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/kms/types#SigningAlgorithmSpec), +// a key ID to use while the AWS SDK makes network +// requests. + +// NewKMS creates a new crypto signer which uses AWS KMS to sign data. The keys signing algorithm spec +// dictates the type of signature that will be generated (see +// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/kms/types#SigningAlgorithmSpec), + +// The key ID is the unique identifier of the KMS key or key alias. +func NewKMS(client *kms.Client, kmsKeyID string) (*KMS, error) { + if kmsKeyID == "" { + return nil, ErrInvalidKeyID + } + + keyDesc, err := client.GetPublicKey(context.Background(), &kms.GetPublicKeyInput{KeyId: aws.String(kmsKeyID)}) + if err != nil { + return nil, fmt.Errorf("failed to describe key %q: %w", kmsKeyID, err) + } + + // the key must be a sign/verify key see https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/kms/types#KeyUsageType + if keyDesc.KeyUsage != types.KeyUsageTypeSignVerify { + return nil, fmt.Errorf("invalid key usage. expected SIGN_VERIFY, got %q", keyDesc.KeyUsage) + } + + // there should be at least one signing algorithm available, and for sign/verify keys there should only be one. + if len(keyDesc.SigningAlgorithms) != 1 { + return nil, fmt.Errorf("expected one signing algorithm for key %q got %q", kmsKeyID, keyDesc.SigningAlgorithms) + } + + alg := keyDesc.SigningAlgorithms[0] + + // Using the matching KMS keyset as per the following table, we select the + // appropriate jwa.KeyAlgorithm see https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 + // and https://docs.aws.amazon.com/kms/latest/developerguide/asymmetric-key-specs.html + // + // | "alg" Param Value | Digital Signature Algorithm | KMS KeySpec | + // | ----------------- | --------------------------- | ----------- | + // | ES256 | ECDSA using P-256 and SHA-256 | ECC_NIST_P256 | + // | ES384 | ECDSA using P-384 and SHA-384 | ECC_NIST_P384 | + // | ES512 | ECDSA using P-521 and SHA-512 | ECC_NIST_P521 | + // | RS256 | RSASSA-PKCS1-v1_5 using SHA-256 | RSASSA_PKCS1_V1_5_SHA_256 | + // | RS384 | RSASSA-PKCS1-v1_5 using SHA-384 | RSASSA_PKCS1_V1_5_SHA_384 | + // | RS512 | RSASSA-PKCS1-v1_5 using SHA-512 | RSASSA_PKCS1_V1_5_SHA_512 | + // + var jwaAlg jwa.KeyAlgorithm + switch alg { + case types.SigningAlgorithmSpecEcdsaSha256: + jwaAlg = jwa.ES256 + case types.SigningAlgorithmSpecEcdsaSha384: + jwaAlg = jwa.ES384 + case types.SigningAlgorithmSpecEcdsaSha512: + jwaAlg = jwa.ES512 + case types.SigningAlgorithmSpecRsassaPkcs1V15Sha256: + jwaAlg = jwa.RS256 + case types.SigningAlgorithmSpecRsassaPkcs1V15Sha384: + jwaAlg = jwa.RS384 + case types.SigningAlgorithmSpecRsassaPkcs1V15Sha512: + jwaAlg = jwa.RS512 + default: + return nil, fmt.Errorf("unsupported signing algorithm %q", alg) + } + + return &KMS{ + client: client, + jwaAlg: jwaAlg, + alg: alg, + kid: kmsKeyID, + }, nil +} + +// Sign generates a signature from the given digest. +func (sv *KMS) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if sv.alg == "" { + return nil, fmt.Errorf("aws.KMS.Sign() requires the types.SigningAlgorithmSpec") + } + if sv.kid == "" { + return nil, fmt.Errorf("aws.KMS.Sign() requires the KMS key ID") + } + + input := kms.SignInput{ + KeyId: aws.String(sv.kid), + Message: digest, + MessageType: types.MessageTypeDigest, + SigningAlgorithm: sv.alg, + } + signed, err := sv.client.Sign(context.Background(), &input) + if err != nil { + return nil, fmt.Errorf("failed to sign via KMS: %w", err) + } + + return signed.Signature, nil +} + +// Public returns the corresponding public key. +// +// NOTE: Because the crypto.Signer API does not allow for an error to be returned, +// the return value from this function cannot describe what kind of error +// occurred. +func (sv *KMS) Public() crypto.PublicKey { + pubkey, _ := sv.GetPublicKey() + return pubkey +} + +// GetPublicKey is an escape hatch for those cases where the user needs +// to debug what went wrong during the GetPublicKey operation. +func (sv *KMS) GetPublicKey() (crypto.PublicKey, error) { + if sv.kid == "" { + return nil, fmt.Errorf("aws.KMS.Sign() requires the key ID") + } + + input := kms.GetPublicKeyInput{ + KeyId: aws.String(sv.kid), + } + + output, err := sv.client.GetPublicKey(context.Background(), &input) + if err != nil { + return nil, fmt.Errorf("failed to get public key from KMS: %w", err) + } + + if output.KeyUsage != types.KeyUsageTypeSignVerify { + return nil, fmt.Errorf("invalid key usage. expected SIGN_VERIFY, got %q", output.KeyUsage) + } + + key, err := x509.ParsePKIXPublicKey(output.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to parse key: %w", err) + } + + return key, nil +} + +// Algorithm returns the equivalent of the KMS key's signing algorithm as a JWA key algorithm. +func (sv *KMS) Algorithm() jwa.KeyAlgorithm { + return sv.jwaAlg +}