diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index db3389c8..09c9099e 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -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 diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 97952911..063aa666 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -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) { diff --git a/authority/provisioner/testdata/certs/aws-test.crt b/authority/provisioner/testdata/certs/aws-test.crt new file mode 100644 index 00000000..2e54e35c --- /dev/null +++ b/authority/provisioner/testdata/certs/aws-test.crt @@ -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----- diff --git a/authority/provisioner/testdata/certs/aws.crt b/authority/provisioner/testdata/certs/aws.crt new file mode 100644 index 00000000..7e3885bd --- /dev/null +++ b/authority/provisioner/testdata/certs/aws.crt @@ -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----- \ No newline at end of file diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 0f4ceb05..d4f5cc80 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -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), diff --git a/docs/provisioners.md b/docs/provisioners.md index ee54845d..fdebc1d1 100644 --- a/docs/provisioners.md +++ b/docs/provisioners.md @@ -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. diff --git a/pki/pki.go b/pki/pki.go index 0576bc00..50ee4e56 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -538,7 +538,7 @@ func (p *PKI) Save(opt ...Option) error { return err } - b, err := json.MarshalIndent(config, "", " ") + b, err := json.MarshalIndent(config, "", "\t") if err != nil { return errors.Wrapf(err, "error marshaling %s", p.config) } @@ -568,7 +568,7 @@ func (p *PKI) Save(opt ...Option) error { CAUrl: p.caURL, Fingerprint: p.rootFingerprint, } - b, err = json.MarshalIndent(defaults, "", " ") + b, err = json.MarshalIndent(defaults, "", "\t") if err != nil { return errors.Wrapf(err, "error marshaling %s", p.defaults) }