From 38bc756235be05c82f749f58c923dd7cf83399f2 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 17 Oct 2024 10:16:05 +0100 Subject: [PATCH] Add initial Signer implementation based on X509 SVID Signed-off-by: Noah Stride --- .github/workflows/release.yaml | 29 +++++++++ aws.md | 18 ++++++ cmd/main.go | 73 ++++++++++++++++------ go.mod | 22 ++++++- go.sum | 50 +++++++++++++++ signer.go | 110 +++++++++++++++++++++++++++++++++ 6 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 aws.md create mode 100644 signer.go diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e69de29..cf1146e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -0,0 +1,29 @@ +name: Release +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + - uses: ko-build/setup-ko@v0.6 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/aws.md b/aws.md new file mode 100644 index 0000000..431ee77 --- /dev/null +++ b/aws.md @@ -0,0 +1,18 @@ +# AWS Roles Anywhere + +## Useful Resources + +- https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication.html +- https://docs.aws.amazon.com/rolesanywhere/latest/userguide/trust-model.html +- https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-sign-process.html + +## Constraints + +End entity certificates must satisfy the following constraints to be used for authentication: +- The certificates MUST be X.509v3. +- Basic constraints MUST include CA: false. +- The key usage MUST include Digital Signature. +- The signing algorithm MUST include SHA256 or stronger. MD5 and SHA1 signing algorithms are rejected. + +> RSA and EC keys are supported; RSA keys are used with the RSA PKCS# v1.5 signing algorithm. EC keys are used with the ECDSA. + diff --git a/cmd/main.go b/cmd/main.go index 292230a..aa368c4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,7 +5,10 @@ import ( "os" "time" + "github.com/aws/rolesanywhere-credential-helper/aws_signing_helper" "github.com/spf13/cobra" + awsspiffe "github.com/spiffe/aws-spiffe-workload-helper" + "github.com/spiffe/go-spiffe/v2/workloadapi" ) var ( @@ -20,6 +23,16 @@ func main() { Version: version, } + x509CredentialProcessCmd := newX509CredentialProcessCmd() + rootCmd.AddCommand(x509CredentialProcessCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} + +func newX509CredentialProcessCmd() *cobra.Command { var ( roleARN string region string @@ -28,28 +41,52 @@ func main() { trustAnchorARN string roleSessionName string ) - x509CredentialProcessCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "x509-credential-process", Short: "TODO", // TODO(strideynet): Helpful, short description. Long: `TODO`, // TODO(strideynet): Helpful, long description. - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Hello, World!") + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := workloadapi.New(ctx) // TODO(strideynet): Ability to configure workload api endpoint with flag + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + + x509Ctx, err := client.FetchX509Context(ctx) + if err != nil { + return fmt.Errorf("fetching x509 context: %w", err) + } + // TODO(strideynet): Implement SVID selection mechanism, for now, + // we'll just use the first returned SVID (a.k.a the default). + svid := x509Ctx.DefaultSVID() + + signer := &awsspiffe.X509SVIDSigner{ + SVID: svid, + } + signatureAlgorithm, err := signer.SignatureAlgorithm() + if err != nil { + return fmt.Errorf("getting signature algorithm: %w", err) + } + aws_signing_helper.GenerateCredentials(&aws_signing_helper.CredentialsOpts{ + RoleArn: roleARN, + ProfileArnStr: profileARN, + Region: region, + RoleSessionName: roleSessionName, + TrustAnchorArnStr: trustAnchorARN, + }, signer, signatureAlgorithm) + + return nil }, } - rootCmd.AddCommand(x509CredentialProcessCmd) // TODO(strideynet): Review flag help strings. - x509CredentialProcessCmd.Flags().StringVar(&roleARN, "role-arn", "", "TODO. Required.") - x509CredentialProcessCmd.MarkFlagRequired("role-arn") - x509CredentialProcessCmd.Flags().StringVar(®ion, "region", "", "TODO") - x509CredentialProcessCmd.Flags().StringVar(&profileARN, "profile-arn", "", "TODO. Required.") - x509CredentialProcessCmd.MarkFlagRequired("profile-arn") - x509CredentialProcessCmd.Flags().DurationVar(&sessionDuration, "session-duration", 0, "TODO") - x509CredentialProcessCmd.Flags().StringVar(&trustAnchorARN, "trust-anchor-arn", "", "TODO. Required.") - x509CredentialProcessCmd.MarkFlagRequired("trust-anchor-arn") - x509CredentialProcessCmd.Flags().StringVar(&roleSessionName, "role-session-name", "", "TODO") - - if err := rootCmd.Execute(); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } + cmd.Flags().StringVar(&roleARN, "role-arn", "", "TODO. Required.") + cmd.MarkFlagRequired("role-arn") + cmd.Flags().StringVar(®ion, "region", "", "TODO") + cmd.Flags().StringVar(&profileARN, "profile-arn", "", "TODO. Required.") + cmd.MarkFlagRequired("profile-arn") + cmd.Flags().DurationVar(&sessionDuration, "session-duration", 0, "TODO") + cmd.Flags().StringVar(&trustAnchorARN, "trust-anchor-arn", "", "TODO. Required.") + cmd.MarkFlagRequired("trust-anchor-arn") + cmd.Flags().StringVar(&roleSessionName, "role-session-name", "", "TODO") + return cmd } diff --git a/go.mod b/go.mod index 38fb015..50f3d03 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,27 @@ module github.com/spiffe/aws-spiffe-workload-helper go 1.22.5 require ( + github.com/aws/rolesanywhere-credential-helper v1.2.0 + github.com/spf13/cobra v1.8.1 + github.com/spiffe/go-spiffe/v2 v2.4.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go v1.55.5 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + github.com/zeebo/errs v1.3.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 912390a..06adbdd 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,60 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/rolesanywhere-credential-helper v1.2.0 h1:eLqJvSznH8nJk48dwFc0raWOpbTGgBeNYH3Q8UQFVx4= +github.com/aws/rolesanywhere-credential-helper v1.2.0/go.mod h1:YRxmRrAaqbVVXPNH1gHT76nWaMGvpAziHAHw8UwKrpU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c= +github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/signer.go b/signer.go new file mode 100644 index 0000000..b5a7e84 --- /dev/null +++ b/signer.go @@ -0,0 +1,110 @@ +package awsspiffe + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "fmt" + "io" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" +) + +// SPIFFESigner creates signatures compatible with the AWS RolesAnywhere +// API using an X509 SVID. It implements the aws_signing_helper.Signer +// interface. +type X509SVIDSigner struct { + SVID *x509svid.SVID +} + +func (s *X509SVIDSigner) Public() crypto.PublicKey { + return s.SVID.PrivateKey.Public() +} + +func (s *X509SVIDSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // Note(strideynet): + // As of the time of writing, it looks like the AWS signing helper will + // only ever invoke Sign with SHA256, however, their signer implementations + // do also support SHA384 and SHA512. It feels safest to support all three + // here as well. + // + // Looking at the documentation for AWS SigV4, it looks like SHA256 is also + // the only supported hash function today... + var hash []byte + switch opts.HashFunc() { + case crypto.SHA256: + sum := sha256.Sum256(digest) + hash = sum[:] + case crypto.SHA384: + sum := sha512.Sum384(digest) + hash = sum[:] + case crypto.SHA512: + sum := sha512.Sum512(digest) + hash = sum[:] + default: + return nil, fmt.Errorf("unsupported hash function: %v", opts.HashFunc()) + } + + // From https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication.html + // > RSA and EC keys are supported; RSA keys are used with the RSA PKCS# + // > v1.5 signing algorithm. EC keys are used with the ECDSA. + switch key := s.SVID.PrivateKey.(type) { + case *rsa.PrivateKey: + sig, err := rsa.SignPKCS1v15(rand, key, opts.HashFunc(), hash) + if err != nil { + return nil, fmt.Errorf("signing with RSA: %w", err) + } + return sig, nil + case *ecdsa.PrivateKey: + sig, err := ecdsa.SignASN1(rand, key, hash) + if err != nil { + return nil, fmt.Errorf("signing with ECDSA: %w", err) + } + return sig, nil + default: + return nil, fmt.Errorf("unsupported key type: %T", s.SVID.PrivateKey) + } +} + +// From https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-sign-process.html +// > Algorithm. As described above, instead of AWS4-HMAC-SHA256, the algorithm +// > field will have the values of the form AWS4-X509-RSA-SHA256 or +// > AWS4-X509-ECDSA-SHA256, depending on whether an RSA or Elliptic Curve +// > algorithm is used. This, in turn, is determined by the key bound to the +// > signing certificate. +const ( + awsV4X509RSASHA256 = "AWS4-X509-RSA-SHA256" + awsV4X509ECDSASHA256 = "AWS4-X509-ECDSA-SHA256" +) + +// SignatureAlgorithm returns the signature algorithm of the underlying +// private key, in the representation expected by AWS. +// See https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-sign-process.html +func (s *X509SVIDSigner) SignatureAlgorithm() (string, error) { + switch s.SVID.PrivateKey.(type) { + case *rsa.PrivateKey: + return awsV4X509RSASHA256, nil + case *ecdsa.PrivateKey: + return awsV4X509ECDSASHA256, nil + default: + return "", fmt.Errorf("unsupported key type: %T", s.SVID.PrivateKey) + } +} + +func (s *X509SVIDSigner) Certificate() (*x509.Certificate, error) { + return s.SVID.Certificates[0], nil +} + +func (s *X509SVIDSigner) CertificateChain() ([]*x509.Certificate, error) { + if len(s.SVID.Certificates) < 1 { + return s.SVID.Certificates[1:], nil + } + return nil, nil +} + +func (s *X509SVIDSigner) Close() { + // Nothing to do here... +}