Skip to content

Commit

Permalink
Add initial Signer implementation based on X509 SVID
Browse files Browse the repository at this point in the history
Signed-off-by: Noah Stride <[email protected]>
  • Loading branch information
strideynet committed Oct 17, 2024
1 parent f3af76a commit 38bc756
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 19 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 changes: 18 additions & 0 deletions aws.md
Original file line number Diff line number Diff line change
@@ -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.
73 changes: 55 additions & 18 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand All @@ -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{

Check failure on line 70 in cmd/main.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `aws_signing_helper.GenerateCredentials` is not checked (errcheck)
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(&region, "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")

Check failure on line 83 in cmd/main.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `cmd.MarkFlagRequired` is not checked (errcheck)
cmd.Flags().StringVar(&region, "region", "", "TODO")
cmd.Flags().StringVar(&profileARN, "profile-arn", "", "TODO. Required.")
cmd.MarkFlagRequired("profile-arn")

Check failure on line 86 in cmd/main.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `cmd.MarkFlagRequired` is not checked (errcheck)
cmd.Flags().DurationVar(&sessionDuration, "session-duration", 0, "TODO")
cmd.Flags().StringVar(&trustAnchorARN, "trust-anchor-arn", "", "TODO. Required.")
cmd.MarkFlagRequired("trust-anchor-arn")

Check failure on line 89 in cmd/main.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `cmd.MarkFlagRequired` is not checked (errcheck)
cmd.Flags().StringVar(&roleSessionName, "role-session-name", "", "TODO")
return cmd
}
22 changes: 21 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
50 changes: 50 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
110 changes: 110 additions & 0 deletions signer.go
Original file line number Diff line number Diff line change
@@ -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...
}

0 comments on commit 38bc756

Please sign in to comment.