Add option to specify the AWS IID certificates to use.

This changes adds a new option `iidRoots` that allows a user to
define one or more certificates that will be used for AWS IID
signature validation.

Fixes #393
This commit is contained in:
Mariano Cano 2020-10-13 17:51:24 -07:00
parent 647b9b4541
commit 7d1686dc53
6 changed files with 156 additions and 31 deletions

View file

@ -75,25 +75,49 @@ type awsConfig struct {
signatureURL string signatureURL string
tokenURL string tokenURL string
tokenTTL string tokenTTL string
certificate *x509.Certificate certificates []*x509.Certificate
signatureAlgorithm x509.SignatureAlgorithm signatureAlgorithm x509.SignatureAlgorithm
} }
func newAWSConfig() (*awsConfig, error) { func newAWSConfig(certPath string) (*awsConfig, error) {
block, _ := pem.Decode([]byte(awsCertificate)) var certBytes []byte
if block == nil || block.Type != "CERTIFICATE" { if certPath == "" {
return nil, errors.New("error decoding AWS certificate") certBytes = []byte(awsCertificate)
} else {
if b, err := ioutil.ReadFile(certPath); err == nil {
certBytes = b
} else {
return nil, errors.Wrapf(err, "error reading %s", certPath)
}
}
// Read all the certificates.
var certs []*x509.Certificate
for len(certBytes) > 0 {
var block *pem.Block
block, certBytes = pem.Decode(certBytes)
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
continue
} }
cert, err := x509.ParseCertificate(block.Bytes) cert, err := x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error parsing AWS certificate") return nil, errors.Wrap(err, "error parsing AWS IID certificate")
} }
certs = append(certs, cert)
}
if len(certs) == 0 {
return nil, errors.New("error parsing AWS IID certificate: no certificates found")
}
return &awsConfig{ return &awsConfig{
identityURL: awsIdentityURL, identityURL: awsIdentityURL,
signatureURL: awsSignatureURL, signatureURL: awsSignatureURL,
tokenURL: awsAPITokenURL, tokenURL: awsAPITokenURL,
tokenTTL: awsAPITokenTTL, tokenTTL: awsAPITokenTTL,
certificate: cert, certificates: certs,
signatureAlgorithm: awsSignatureAlgorithm, signatureAlgorithm: awsSignatureAlgorithm,
}, nil }, nil
} }
@ -140,6 +164,9 @@ type awsInstanceIdentityDocument struct {
// If InstanceAge is set, only the instances with a pendingTime within the given // If InstanceAge is set, only the instances with a pendingTime within the given
// period will be accepted. // period will be accepted.
// //
// IIDRoots can be used to specify a path to the certificates used to verify the
// identity certificate signature.
//
// Amazon Identity docs are available at // Amazon Identity docs are available at
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
type AWS struct { type AWS struct {
@ -151,6 +178,7 @@ type AWS struct {
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
IMDSVersions []string `json:"imdsVersions"` IMDSVersions []string `json:"imdsVersions"`
InstanceAge Duration `json:"instanceAge,omitempty"` InstanceAge Duration `json:"instanceAge,omitempty"`
IIDRoots string `json:"iidRoots,omitempty"`
Claims *Claims `json:"claims,omitempty"` Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"` Options *Options `json:"options,omitempty"`
claimer *Claimer claimer *Claimer
@ -260,7 +288,7 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) {
tok, err := jose.Signed(signer).Claims(payload).CompactSerialize() tok, err := jose.Signed(signer).Claims(payload).CompactSerialize()
if err != nil { if err != nil {
return "", errors.Wrap(err, "error serialiazing token") return "", errors.Wrap(err, "error serializing token")
} }
return tok, nil return tok, nil
@ -281,7 +309,7 @@ func (p *AWS) Init(config Config) (err error) {
return err return err
} }
// Add default config // Add default config
if p.config, err = newAWSConfig(); err != nil { if p.config, err = newAWSConfig(p.IIDRoots); err != nil {
return err return err
} }
p.audiences = config.Audiences.WithFragment(p.GetID()) p.audiences = config.Audiences.WithFragment(p.GetID())
@ -371,17 +399,19 @@ func (p *AWS) assertConfig() (err error) {
if p.config != nil { if p.config != nil {
return return
} }
p.config, err = newAWSConfig() p.config, err = newAWSConfig(p.IIDRoots)
return err return err
} }
// checkSignature returns an error if the signature is not valid. // checkSignature returns an error if the signature is not valid.
func (p *AWS) checkSignature(signed, signature []byte) error { func (p *AWS) checkSignature(signed, signature []byte) error {
if err := p.config.certificate.CheckSignature(p.config.signatureAlgorithm, signed, signature); err != nil { for _, crt := range p.config.certificates {
return errors.Wrap(err, "error validating identity document signature") if err := crt.CheckSignature(p.config.signatureAlgorithm, signed, signature); err == nil {
}
return nil return nil
} }
}
return errors.New("error validating identity document signature")
}
// readURL does a GET request to the given url and returns the body. It's not // readURL does a GET request to the given url and returns the body. It's not
// using pkg/errors to avoid verbose errors, the caller should use it and write // using pkg/errors to avoid verbose errors, the caller should use it and write

View file

@ -178,8 +178,12 @@ func TestAWS_GetIdentityToken(t *testing.T) {
assert.Equals(t, tt.args.subject, c.Subject) assert.Equals(t, tt.args.subject, c.Subject)
assert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: "/1.0/sign", Fragment: tt.aws.GetID()}).String()}, c.Audience) assert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: "/1.0/sign", Fragment: tt.aws.GetID()}).String()}, c.Audience)
assert.Equals(t, tt.aws.Accounts[0], c.document.AccountID) assert.Equals(t, tt.aws.Accounts[0], c.document.AccountID)
err = tt.aws.config.certificate.CheckSignature( for _, crt := range tt.aws.config.certificates {
tt.aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature) err = crt.CheckSignature(tt.aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)
if err == nil {
break
}
}
assert.NoError(t, err) assert.NoError(t, err)
} }
} }
@ -206,8 +210,12 @@ func TestAWS_GetIdentityToken_V1Only(t *testing.T) {
assert.Equals(t, subject, c.Subject) assert.Equals(t, subject, c.Subject)
assert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: "/1.0/sign", Fragment: aws.GetID()}).String()}, c.Audience) assert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: "/1.0/sign", Fragment: aws.GetID()}).String()}, c.Audience)
assert.Equals(t, aws.Accounts[0], c.document.AccountID) assert.Equals(t, aws.Accounts[0], c.document.AccountID)
err = aws.config.certificate.CheckSignature( for _, crt := range aws.config.certificates {
aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature) err = crt.CheckSignature(aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)
if err == nil {
break
}
}
assert.NoError(t, err) assert.NoError(t, err)
} }
} }
@ -247,6 +255,7 @@ func TestAWS_Init(t *testing.T) {
DisableTrustOnFirstUse bool DisableTrustOnFirstUse bool
InstanceAge Duration InstanceAge Duration
IMDSVersions []string IMDSVersions []string
IIDRoots string
Claims *Claims Claims *Claims
} }
type args struct { type args struct {
@ -258,16 +267,19 @@ func TestAWS_Init(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
{"ok", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, nil}, args{config}, false}, {"ok", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "", nil}, args{config}, false},
{"ok/v1", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1"}, nil}, args{config}, false}, {"ok/v1", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1"}, "", nil}, args{config}, false},
{"ok/v2", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v2"}, nil}, args{config}, false}, {"ok/v2", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v2"}, "", nil}, args{config}, false},
{"ok/empty", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{}, nil}, args{config}, false}, {"ok/empty", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{}, "", nil}, args{config}, false},
{"ok/duration", fields{"AWS", "name", []string{"account"}, true, true, Duration{Duration: 1 * time.Minute}, []string{"v1", "v2"}, nil}, args{config}, false}, {"ok/duration", fields{"AWS", "name", []string{"account"}, true, true, Duration{Duration: 1 * time.Minute}, []string{"v1", "v2"}, "", nil}, args{config}, false},
{"fail type ", fields{"", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, nil}, args{config}, true}, {"ok/cert", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "testdata/certs/aws.crt", nil}, args{config}, false},
{"fail name", fields{"AWS", "", []string{"account"}, false, false, zero, []string{"v1", "v2"}, nil}, args{config}, true}, {"fail type ", fields{"", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "", nil}, args{config}, true},
{"bad instance age", fields{"AWS", "name", []string{"account"}, false, false, Duration{Duration: -1 * time.Minute}, []string{"v1", "v2"}, nil}, args{config}, true}, {"fail name", fields{"AWS", "", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "", nil}, args{config}, true},
{"fail/imds", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"bad"}, nil}, args{config}, true}, {"bad instance age", fields{"AWS", "name", []string{"account"}, false, false, Duration{Duration: -1 * time.Minute}, []string{"v1", "v2"}, "", nil}, args{config}, true},
{"fail claims", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, badClaims}, args{config}, true}, {"fail/imds", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"bad"}, "", nil}, args{config}, true},
{"fail/missing", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"bad"}, "testdata/missing.crt", nil}, args{config}, true},
{"fail/cert", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"bad"}, "testdata/certs/rsa.csr", nil}, args{config}, true},
{"fail claims", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "", badClaims}, args{config}, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -279,6 +291,7 @@ func TestAWS_Init(t *testing.T) {
DisableTrustOnFirstUse: tt.fields.DisableTrustOnFirstUse, DisableTrustOnFirstUse: tt.fields.DisableTrustOnFirstUse,
InstanceAge: tt.fields.InstanceAge, InstanceAge: tt.fields.InstanceAge,
IMDSVersions: tt.fields.IMDSVersions, IMDSVersions: tt.fields.IMDSVersions,
IIDRoots: tt.fields.IIDRoots,
Claims: tt.fields.Claims, Claims: tt.fields.Claims,
} }
if err := p.Init(tt.args.config); (err != nil) != tt.wantErr { if err := p.Init(tt.args.config); (err != nil) != tt.wantErr {
@ -457,6 +470,19 @@ func TestAWS_authorizeToken(t *testing.T) {
err: errors.New("aws.authorizeToken; aws identity document pendingTime is too old"), err: errors.New("aws.authorizeToken; aws identity document pendingTime is too old"),
} }
}, },
"fail/identityCert": func(t *testing.T) test {
p, err := generateAWS()
p.IIDRoots = "testdata/certs/aws.crt"
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
p, err := generateAWS() p, err := generateAWS()
assert.FatalError(t, err) assert.FatalError(t, err)
@ -469,6 +495,19 @@ func TestAWS_authorizeToken(t *testing.T) {
token: tok, token: tok,
} }
}, },
"ok/identityCert": func(t *testing.T) test {
p, err := generateAWS()
p.IIDRoots = "testdata/certs/aws-test.crt"
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
} }
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {

View file

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw
GDEWMBQGA1UEAxMNQVdTIFRlc3QgQ2VydDAeFw0xOTA0MjQyMjU3MzlaFw0yOTA0
MjEyMjU3MzlaMBgxFjAUBgNVBAMTDUFXUyBUZXN0IENlcnQwgZ8wDQYJKoZIhvcN
AQEBBQADgY0AMIGJAoGBAOHMmMXwbXN90SoRl/xXAcJs5TacaVYJ5iNAVWM5KYyF
+JwqYuJp/umLztFUi0oX0luu3EzD4KurVeUJSzZjTFTX1d/NX6hA45+bvdSUOcgV
UghO+2uhBZ4SNFxFRZ7SKvoWIN195l5bVX6/60Eo6+kUCKCkyxW4V/ksWzdXjHnf
AgMBAAGjXzBdMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0G
A1UdDgQWBBRHfLOjEddK/CWCIHNg8Oc/oJa1IzAYBgNVHREEETAPgg1BV1MgVGVz
dCBDZXJ0MA0GCSqGSIb3DQEBCwUAA4GBAKNCiVM9eGb9dW2xNyHaHAmmy7ERB2OJ
7oXHfLjooOavk9lU/Gs2jfX/JSBa84+DzWg9ShmCNLti8CxU/dhzXW7jE/5CcdTa
DCA6B3Yl5TmfG9+D9dtFqRB2CiMgNcsJJE5Dc6pDwBIiSj/MkE0AaGVQmSwn6Cb6
vX1TAxqeWJHq
-----END CERTIFICATE-----

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
-----END CERTIFICATE-----

View file

@ -419,7 +419,7 @@ func generateAWS() (*AWS, error) {
signatureURL: awsSignatureURL, signatureURL: awsSignatureURL,
tokenURL: awsAPITokenURL, tokenURL: awsAPITokenURL,
tokenTTL: awsAPITokenTTL, tokenTTL: awsAPITokenTTL,
certificate: cert, certificates: []*x509.Certificate{cert},
signatureAlgorithm: awsSignatureAlgorithm, signatureAlgorithm: awsSignatureAlgorithm,
}, },
audiences: testAudiences.WithFragment("aws/" + name), audiences: testAudiences.WithFragment("aws/" + name),
@ -528,7 +528,7 @@ func generateAWSV1Only() (*AWS, error) {
signatureURL: awsSignatureURL, signatureURL: awsSignatureURL,
tokenURL: awsAPITokenURL, tokenURL: awsAPITokenURL,
tokenTTL: awsAPITokenTTL, tokenTTL: awsAPITokenTTL,
certificate: cert, certificates: []*x509.Certificate{cert},
signatureAlgorithm: awsSignatureAlgorithm, signatureAlgorithm: awsSignatureAlgorithm,
}, },
audiences: testAudiences.WithFragment("aws/" + name), audiences: testAudiences.WithFragment("aws/" + name),

View file

@ -441,6 +441,7 @@ In the ca.json, an AWS provisioner looks like:
"disableCustomSANs": false, "disableCustomSANs": false,
"disableTrustOnFirstUse": false, "disableTrustOnFirstUse": false,
"instanceAge": "1h", "instanceAge": "1h",
"iidRoots": "/path/to/aws.crt",
"claims": { "claims": {
"maxTLSCertDuration": "2160h", "maxTLSCertDuration": "2160h",
"defaultTLSCertDuration": "2160h" "defaultTLSCertDuration": "2160h"
@ -468,6 +469,9 @@ In the ca.json, an AWS provisioner looks like:
* `instanceAge` (optional): the maximum age of an instance to grant a * `instanceAge` (optional): the maximum age of an instance to grant a
certificate. The instance age is a string using the duration format. certificate. The instance age is a string using the duration format.
* `iidRoots` (optional): the path to one or more public certificates in PEM
format used to validate the signature of the instance identity document.
* `claims` (optional): overwrites the default claims set in the authority, see * `claims` (optional): overwrites the default claims set in the authority, see
the [top](#provisioners) section for all the options. the [top](#provisioners) section for all the options.