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:
parent
647b9b4541
commit
7d1686dc53
6 changed files with 156 additions and 31 deletions
|
@ -75,25 +75,49 @@ type awsConfig struct {
|
|||
signatureURL string
|
||||
tokenURL string
|
||||
tokenTTL string
|
||||
certificate *x509.Certificate
|
||||
certificates []*x509.Certificate
|
||||
signatureAlgorithm x509.SignatureAlgorithm
|
||||
}
|
||||
|
||||
func newAWSConfig() (*awsConfig, error) {
|
||||
block, _ := pem.Decode([]byte(awsCertificate))
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return nil, errors.New("error decoding AWS certificate")
|
||||
func newAWSConfig(certPath string) (*awsConfig, error) {
|
||||
var certBytes []byte
|
||||
if certPath == "" {
|
||||
certBytes = []byte(awsCertificate)
|
||||
} else {
|
||||
if b, err := ioutil.ReadFile(certPath); err == nil {
|
||||
certBytes = b
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "error reading %s", certPath)
|
||||
}
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing AWS certificate")
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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{
|
||||
identityURL: awsIdentityURL,
|
||||
signatureURL: awsSignatureURL,
|
||||
tokenURL: awsAPITokenURL,
|
||||
tokenTTL: awsAPITokenTTL,
|
||||
certificate: cert,
|
||||
certificates: certs,
|
||||
signatureAlgorithm: awsSignatureAlgorithm,
|
||||
}, nil
|
||||
}
|
||||
|
@ -140,6 +164,9 @@ type awsInstanceIdentityDocument struct {
|
|||
// If InstanceAge is set, only the instances with a pendingTime within the given
|
||||
// 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
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
||||
type AWS struct {
|
||||
|
@ -151,6 +178,7 @@ type AWS struct {
|
|||
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
|
||||
IMDSVersions []string `json:"imdsVersions"`
|
||||
InstanceAge Duration `json:"instanceAge,omitempty"`
|
||||
IIDRoots string `json:"iidRoots,omitempty"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
Options *Options `json:"options,omitempty"`
|
||||
claimer *Claimer
|
||||
|
@ -260,7 +288,7 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) {
|
|||
|
||||
tok, err := jose.Signed(signer).Claims(payload).CompactSerialize()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error serialiazing token")
|
||||
return "", errors.Wrap(err, "error serializing token")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
|
@ -281,7 +309,7 @@ func (p *AWS) Init(config Config) (err error) {
|
|||
return err
|
||||
}
|
||||
// Add default config
|
||||
if p.config, err = newAWSConfig(); err != nil {
|
||||
if p.config, err = newAWSConfig(p.IIDRoots); err != nil {
|
||||
return err
|
||||
}
|
||||
p.audiences = config.Audiences.WithFragment(p.GetID())
|
||||
|
@ -371,16 +399,18 @@ func (p *AWS) assertConfig() (err error) {
|
|||
if p.config != nil {
|
||||
return
|
||||
}
|
||||
p.config, err = newAWSConfig()
|
||||
p.config, err = newAWSConfig(p.IIDRoots)
|
||||
return err
|
||||
}
|
||||
|
||||
// checkSignature returns an error if the signature is not valid.
|
||||
func (p *AWS) checkSignature(signed, signature []byte) error {
|
||||
if err := p.config.certificate.CheckSignature(p.config.signatureAlgorithm, signed, signature); err != nil {
|
||||
return errors.Wrap(err, "error validating identity document signature")
|
||||
for _, crt := range p.config.certificates {
|
||||
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
|
||||
|
|
|
@ -178,8 +178,12 @@ func TestAWS_GetIdentityToken(t *testing.T) {
|
|||
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, tt.aws.Accounts[0], c.document.AccountID)
|
||||
err = tt.aws.config.certificate.CheckSignature(
|
||||
tt.aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)
|
||||
for _, crt := range tt.aws.config.certificates {
|
||||
err = crt.CheckSignature(tt.aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
@ -206,8 +210,12 @@ func TestAWS_GetIdentityToken_V1Only(t *testing.T) {
|
|||
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, aws.Accounts[0], c.document.AccountID)
|
||||
err = aws.config.certificate.CheckSignature(
|
||||
aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)
|
||||
for _, crt := range aws.config.certificates {
|
||||
err = crt.CheckSignature(aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +255,7 @@ func TestAWS_Init(t *testing.T) {
|
|||
DisableTrustOnFirstUse bool
|
||||
InstanceAge Duration
|
||||
IMDSVersions []string
|
||||
IIDRoots string
|
||||
Claims *Claims
|
||||
}
|
||||
type args struct {
|
||||
|
@ -258,16 +267,19 @@ func TestAWS_Init(t *testing.T) {
|
|||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"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/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/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},
|
||||
{"fail name", fields{"AWS", "", []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/imds", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"bad"}, nil}, args{config}, true},
|
||||
{"fail claims", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, badClaims}, args{config}, true},
|
||||
{"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/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/duration", fields{"AWS", "name", []string{"account"}, true, true, Duration{Duration: 1 * time.Minute}, []string{"v1", "v2"}, "", nil}, args{config}, false},
|
||||
{"ok/cert", fields{"AWS", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "testdata/certs/aws.crt", nil}, args{config}, false},
|
||||
{"fail type ", fields{"", "name", []string{"account"}, false, false, zero, []string{"v1", "v2"}, "", nil}, args{config}, true},
|
||||
{"fail name", fields{"AWS", "", []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/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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -279,6 +291,7 @@ func TestAWS_Init(t *testing.T) {
|
|||
DisableTrustOnFirstUse: tt.fields.DisableTrustOnFirstUse,
|
||||
InstanceAge: tt.fields.InstanceAge,
|
||||
IMDSVersions: tt.fields.IMDSVersions,
|
||||
IIDRoots: tt.fields.IIDRoots,
|
||||
Claims: tt.fields.Claims,
|
||||
}
|
||||
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"),
|
||||
}
|
||||
},
|
||||
"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 {
|
||||
p, err := generateAWS()
|
||||
assert.FatalError(t, err)
|
||||
|
@ -469,6 +495,19 @@ func TestAWS_authorizeToken(t *testing.T) {
|
|||
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 {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
|
33
authority/provisioner/testdata/certs/aws-test.crt
vendored
Normal file
33
authority/provisioner/testdata/certs/aws-test.crt
vendored
Normal 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-----
|
19
authority/provisioner/testdata/certs/aws.crt
vendored
Normal file
19
authority/provisioner/testdata/certs/aws.crt
vendored
Normal 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-----
|
|
@ -419,7 +419,7 @@ func generateAWS() (*AWS, error) {
|
|||
signatureURL: awsSignatureURL,
|
||||
tokenURL: awsAPITokenURL,
|
||||
tokenTTL: awsAPITokenTTL,
|
||||
certificate: cert,
|
||||
certificates: []*x509.Certificate{cert},
|
||||
signatureAlgorithm: awsSignatureAlgorithm,
|
||||
},
|
||||
audiences: testAudiences.WithFragment("aws/" + name),
|
||||
|
@ -528,7 +528,7 @@ func generateAWSV1Only() (*AWS, error) {
|
|||
signatureURL: awsSignatureURL,
|
||||
tokenURL: awsAPITokenURL,
|
||||
tokenTTL: awsAPITokenTTL,
|
||||
certificate: cert,
|
||||
certificates: []*x509.Certificate{cert},
|
||||
signatureAlgorithm: awsSignatureAlgorithm,
|
||||
},
|
||||
audiences: testAudiences.WithFragment("aws/" + name),
|
||||
|
|
|
@ -441,6 +441,7 @@ In the ca.json, an AWS provisioner looks like:
|
|||
"disableCustomSANs": false,
|
||||
"disableTrustOnFirstUse": false,
|
||||
"instanceAge": "1h",
|
||||
"iidRoots": "/path/to/aws.crt",
|
||||
"claims": {
|
||||
"maxTLSCertDuration": "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
|
||||
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
|
||||
the [top](#provisioners) section for all the options.
|
||||
|
||||
|
|
Loading…
Reference in a new issue