From af0f21d7440551f4b57c2d7effc5b639af3efff6 Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Tue, 19 May 2020 18:42:12 -0400 Subject: [PATCH 01/20] added support for IMDSv2 API --- authority/provisioner/aws.go | 40 +++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 3f2c8873..221634a4 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -29,6 +29,19 @@ const awsIdentityURL = "http://169.254.169.254/latest/dynamic/instance-identity/ // awsSignatureURL is the url used to retrieve the instance identity signature. const awsSignatureURL = "http://169.254.169.254/latest/dynamic/instance-identity/signature" +// awsAPITokenURL is the url used to get the IMDSv2 API token +const awsAPITokenURL = "http://169.254.169.254/latest/api/token" + +// awsAPITokenTTL is the default TTL to use when requesting IMDSv2 API tokens +// -- we keep this short-lived since we get a new token with every call to readURL() +const awsAPITokenTTL = "30" + +// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request +const awsMetadataTokenHeader = "X-aws-ec2-metadata-token" + +// awsMetadataTokenTTLHeader is the header used to indicate the token TTL requested +const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds" + // awsCertificate is the certificate used to validate the instance identity // signature. const awsCertificate = `-----BEGIN CERTIFICATE----- @@ -332,7 +345,15 @@ func (p *AWS) checkSignature(signed, signature []byte) error { // using pkg/errors to avoid verbose errors, the caller should use it and write // the appropriate error. func (p *AWS) readURL(url string) ([]byte, error) { - r, err := http.Get(url) + client := &http.Client{} + + // get authorization token + req, err := http.NewRequest(http.MethodPut, awsAPITokenURL, nil) + if err != nil { + return nil, err + } + req.Header.Set(awsMetadataTokenTTLHeader, awsAPITokenTTL) + r, err := client.Do(req) if err != nil { return nil, err } @@ -341,6 +362,23 @@ func (p *AWS) readURL(url string) ([]byte, error) { if err != nil { return nil, err } + token := string(b) + + // now get the data + req, err = http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set(awsMetadataTokenHeader, token) + r, err = client.Do(req) + if err != nil { + return nil, err + } + defer r.Body.Close() + b, err = ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } return b, nil } From bbbe4738c77fc6c8a7845267f5a816fdb766ca22 Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Tue, 19 May 2020 23:57:09 -0400 Subject: [PATCH 02/20] Added status code checking --- authority/provisioner/aws.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 221634a4..c2c1fab9 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -196,14 +196,14 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) { var idoc awsInstanceIdentityDocument doc, err := p.readURL(p.config.identityURL) if err != nil { - return "", errors.Wrap(err, "error retrieving identity document, are you in an AWS VM?") + return "", errors.Wrap(err, "error retrieving identity document, are you in an AWS VM with IMDSv2 enabled?") } if err := json.Unmarshal(doc, &idoc); err != nil { return "", errors.Wrap(err, "error unmarshaling identity document") } sig, err := p.readURL(p.config.signatureURL) if err != nil { - return "", errors.Wrap(err, "error retrieving identity document signature, are you in an AWS VM?") + return "", errors.Wrap(err, "error retrieving identity document signature, are you in an AWS VM with IMDSv2 enabled?") } signature, err := base64.StdEncoding.DecodeString(string(sig)) if err != nil { @@ -358,6 +358,9 @@ func (p *AWS) readURL(url string) ([]byte, error) { return nil, err } defer r.Body.Close() + if r.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP request returned non-successful status code %d", r.StatusCode) + } b, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err @@ -375,6 +378,9 @@ func (p *AWS) readURL(url string) ([]byte, error) { return nil, err } defer r.Body.Close() + if r.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP request returned non-successful status code %d", r.StatusCode) + } b, err = ioutil.ReadAll(r.Body) if err != nil { return nil, err From dd27901b12e32defd01346e2525db0f623179439 Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Wed, 20 May 2020 09:03:35 -0400 Subject: [PATCH 03/20] Moved token URL and TTL to config values --- authority/provisioner/aws.go | 8 ++++++-- authority/provisioner/utils_test.go | 14 ++++++++++++++ go.mod | 2 +- go.sum | 11 +++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index c2c1fab9..f3b539d6 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -71,6 +71,8 @@ const awsSignatureAlgorithm = x509.SHA256WithRSA type awsConfig struct { identityURL string signatureURL string + tokenURL string + tokenTTL string certificate *x509.Certificate signatureAlgorithm x509.SignatureAlgorithm } @@ -87,6 +89,8 @@ func newAWSConfig() (*awsConfig, error) { return &awsConfig{ identityURL: awsIdentityURL, signatureURL: awsSignatureURL, + tokenURL: awsAPITokenURL, + tokenTTL: awsAPITokenTTL, certificate: cert, signatureAlgorithm: awsSignatureAlgorithm, }, nil @@ -348,11 +352,11 @@ func (p *AWS) readURL(url string) ([]byte, error) { client := &http.Client{} // get authorization token - req, err := http.NewRequest(http.MethodPut, awsAPITokenURL, nil) + req, err := http.NewRequest(http.MethodPut, p.config.tokenURL, nil) if err != nil { return nil, err } - req.Header.Set(awsMetadataTokenTTLHeader, awsAPITokenTTL) + req.Header.Set(awsMetadataTokenTTLHeader, p.config.tokenTTL) r, err := client.Do(req) if err != nil { return nil, err diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 418e6a5b..0ef80314 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -457,12 +457,25 @@ func generateAWSWithServer() (*AWS, *httptest.Server, error) { if err != nil { return nil, nil, errors.Wrap(err, "error signing document") } + token := "AQAEAEEO9-7Z88ewKFpboZuDlFYWz9A3AN-wMOVzjEhfAyXW31BvVw==" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/latest/dynamic/instance-identity/document": + // check for API token + if r.Header.Get("X-aws-ec2-metadata-token") != token { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("401 Unauthorized")) + } w.Write(doc) case "/latest/dynamic/instance-identity/signature": + // check for API token + if r.Header.Get("X-aws-ec2-metadata-token") != token { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("401 Unauthorized")) + } w.Write([]byte(base64.StdEncoding.EncodeToString(signature))) + case "/latest/api/token": + w.Write([]byte(token)) case "/bad-document": w.Write([]byte("{}")) case "/bad-signature": @@ -475,6 +488,7 @@ func generateAWSWithServer() (*AWS, *httptest.Server, error) { })) aws.config.identityURL = srv.URL + "/latest/dynamic/instance-identity/document" aws.config.signatureURL = srv.URL + "/latest/dynamic/instance-identity/signature" + aws.config.tokenURL = srv.URL + "/latest/api/token" return aws, srv, nil } diff --git a/go.mod b/go.mod index c81244cf..8cf58e89 100644 --- a/go.mod +++ b/go.mod @@ -26,5 +26,5 @@ require ( gopkg.in/square/go-jose.v2 v2.4.0 ) -//replace github.com/smallstep/cli => ../cli +replace github.com/smallstep/cli => ../cli //replace github.com/smallstep/nosql => ../nosql diff --git a/go.sum b/go.sum index 7a9caaef..2f95d40a 100644 --- a/go.sum +++ b/go.sum @@ -21,12 +21,16 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk= github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= +github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= @@ -52,6 +56,7 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bombsimon/wsl/v2 v2.0.0 h1:+Vjcn+/T5lSrO8Bjzhk4v14Un/2UyCA1E3V5j9nwTkQ= github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -65,6 +70,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -80,6 +86,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -268,6 +275,7 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -282,6 +290,7 @@ github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= @@ -364,6 +373,7 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1 github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= @@ -778,6 +788,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= From 226cc6ab48eef50900144188d9f0f8b77b930a5a Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Wed, 20 May 2020 09:32:04 -0400 Subject: [PATCH 04/20] reverted --- go.mod | 2 +- go.sum | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 8cf58e89..c81244cf 100644 --- a/go.mod +++ b/go.mod @@ -26,5 +26,5 @@ require ( gopkg.in/square/go-jose.v2 v2.4.0 ) -replace github.com/smallstep/cli => ../cli +//replace github.com/smallstep/cli => ../cli //replace github.com/smallstep/nosql => ../nosql diff --git a/go.sum b/go.sum index 2f95d40a..7a9caaef 100644 --- a/go.sum +++ b/go.sum @@ -21,16 +21,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= -github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk= github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g= github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U= -github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= @@ -56,7 +52,6 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bombsimon/wsl/v2 v2.0.0 h1:+Vjcn+/T5lSrO8Bjzhk4v14Un/2UyCA1E3V5j9nwTkQ= github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -70,7 +65,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -86,7 +80,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -275,7 +268,6 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -290,7 +282,6 @@ github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= @@ -373,7 +364,6 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1 github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= @@ -788,7 +778,6 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= From 8c6a46887baf700592dbca69f236749057c02002 Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Wed, 20 May 2020 09:39:19 -0400 Subject: [PATCH 05/20] Added token URL fixes to tests --- authority/provisioner/aws_test.go | 6 ++++++ authority/provisioner/utils_test.go | 2 ++ 2 files changed, 8 insertions(+) diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 9e8fc7ad..21620053 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -103,36 +103,42 @@ func TestAWS_GetIdentityToken(t *testing.T) { p2.Accounts = p1.Accounts p2.config.identityURL = srv.URL + "/bad-document" p2.config.signatureURL = p1.config.signatureURL + p2.config.tokenURL = p1.config.tokenURL p3, err := generateAWS() assert.FatalError(t, err) p3.Accounts = p1.Accounts p3.config.signatureURL = srv.URL p3.config.identityURL = p1.config.identityURL + p3.config.tokenURL = p1.config.tokenURL p4, err := generateAWS() assert.FatalError(t, err) p4.Accounts = p1.Accounts p4.config.signatureURL = srv.URL + "/bad-signature" p4.config.identityURL = p1.config.identityURL + p4.config.tokenURL = p1.config.tokenURL p5, err := generateAWS() assert.FatalError(t, err) p5.Accounts = p1.Accounts p5.config.identityURL = "https://1234.1234.1234.1234" p5.config.signatureURL = p1.config.signatureURL + p5.config.tokenURL = p1.config.tokenURL p6, err := generateAWS() assert.FatalError(t, err) p6.Accounts = p1.Accounts p6.config.identityURL = p1.config.identityURL p6.config.signatureURL = "https://1234.1234.1234.1234" + p6.config.tokenURL = p1.config.tokenURL p7, err := generateAWS() assert.FatalError(t, err) p7.Accounts = p1.Accounts p7.config.identityURL = srv.URL + "/bad-json" p7.config.signatureURL = p1.config.signatureURL + p7.config.tokenURL = p1.config.tokenURL caURL := "https://ca.smallstep.com" u, err := url.Parse(caURL) diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 0ef80314..23e8d86d 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -416,6 +416,8 @@ func generateAWS() (*AWS, error) { config: &awsConfig{ identityURL: awsIdentityURL, signatureURL: awsSignatureURL, + tokenURL: awsAPITokenURL, + tokenTTL: awsAPITokenTTL, certificate: cert, signatureAlgorithm: awsSignatureAlgorithm, }, From 18ac5c07e222b3e8af7bd765b1bc8c084ef3d68d Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Wed, 20 May 2020 13:15:51 -0400 Subject: [PATCH 06/20] Added support for specifying IMDS version preference --- authority/provisioner/aws.go | 81 ++++++++++++++++++++++------- authority/provisioner/utils_test.go | 11 ++-- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index f3b539d6..9da6a9f3 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -147,6 +147,7 @@ type AWS struct { Accounts []string `json:"accounts"` DisableCustomSANs bool `json:"disableCustomSANs"` DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` + IMDSVersions []string `json:"imdsVersions"` InstanceAge Duration `json:"instanceAge,omitempty"` Claims *Claims `json:"claims,omitempty"` claimer *Claimer @@ -200,14 +201,14 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) { var idoc awsInstanceIdentityDocument doc, err := p.readURL(p.config.identityURL) if err != nil { - return "", errors.Wrap(err, "error retrieving identity document, are you in an AWS VM with IMDSv2 enabled?") + return "", errors.Wrap(err, "error retrieving identity document, are you in an AWS VM using the proper IMDS version?") } if err := json.Unmarshal(doc, &idoc); err != nil { return "", errors.Wrap(err, "error unmarshaling identity document") } sig, err := p.readURL(p.config.signatureURL) if err != nil { - return "", errors.Wrap(err, "error retrieving identity document signature, are you in an AWS VM with IMDSv2 enabled?") + return "", errors.Wrap(err, "error retrieving identity document signature, are you in an AWS VM using the proper IMDS version?") } signature, err := base64.StdEncoding.DecodeString(string(sig)) if err != nil { @@ -349,43 +350,87 @@ func (p *AWS) checkSignature(signed, signature []byte) error { // using pkg/errors to avoid verbose errors, the caller should use it and write // the appropriate error. func (p *AWS) readURL(url string) ([]byte, error) { - client := &http.Client{} + var resp *http.Response + var err error + + for _, v := range p.IMDSVersions { + switch v { + case "v1": + resp, err = p.readURLv1(url) + if err == nil && resp.StatusCode < 400 { + return p.readResponseBody(resp) + } + case "v2": + resp, err = p.readURLv2(url) + if err == nil && resp.StatusCode < 400 { + return p.readResponseBody(resp) + } + default: + return nil, fmt.Errorf("%s: not a supported AWS Instance Metadata Service version", v) + } + } + + // all versions have been exhausted and we haven't returned successfully yet so pass + // the error on to the caller + if err != nil { + return nil, err + } + return nil, fmt.Errorf("Request for metadata returned non-successful status code %d", + resp.StatusCode) +} + +func (p *AWS) readURLv1(url string) (*http.Response, error) { + client := http.Client{} + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} + +func (p *AWS) readURLv2(url string) (*http.Response, error) { + client := http.Client{} - // get authorization token + // first get the token req, err := http.NewRequest(http.MethodPut, p.config.tokenURL, nil) if err != nil { return nil, err } req.Header.Set(awsMetadataTokenTTLHeader, p.config.tokenTTL) - r, err := client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, err } - defer r.Body.Close() - if r.StatusCode >= 400 { - return nil, fmt.Errorf("HTTP request returned non-successful status code %d", r.StatusCode) + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("Request for API token returned non-successful status code %d", resp.StatusCode) } - b, err := ioutil.ReadAll(r.Body) + token, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } - token := string(b) - // now get the data + // now make the request req, err = http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } - req.Header.Set(awsMetadataTokenHeader, token) - r, err = client.Do(req) + req.Header.Set(awsMetadataTokenHeader, string(token)) + resp, err = client.Do(req) if err != nil { return nil, err } - defer r.Body.Close() - if r.StatusCode >= 400 { - return nil, fmt.Errorf("HTTP request returned non-successful status code %d", r.StatusCode) - } - b, err = ioutil.ReadAll(r.Body) + return resp, nil +} + +func (p *AWS) readResponseBody(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 23e8d86d..84283631 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -408,11 +408,12 @@ func generateAWS() (*AWS, error) { return nil, errors.Wrap(err, "error parsing AWS certificate") } return &AWS{ - Type: "AWS", - Name: name, - Accounts: []string{accountID}, - Claims: &globalProvisionerClaims, - claimer: claimer, + Type: "AWS", + Name: name, + Accounts: []string{accountID}, + Claims: &globalProvisionerClaims, + IMDSVersions: []string{"v2", "v1"}, + claimer: claimer, config: &awsConfig{ identityURL: awsIdentityURL, signatureURL: awsSignatureURL, From 044d00045a14026fa66d796c4fc7144047a46f96 Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Wed, 20 May 2020 13:24:45 -0400 Subject: [PATCH 07/20] Fixed missing initialization of IMDS versions --- authority/provisioner/aws.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 9da6a9f3..52c8a40d 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -282,6 +282,22 @@ func (p *AWS) Init(config Config) (err error) { return err } p.audiences = config.Audiences.WithFragment(p.GetID()) + + // validate IMDS versions + if len(p.IMDSVersions) == 0 { + p.IMDSVersions = []string{"v2", "v1"} + } + for _, v := range p.IMDSVersions { + switch v { + case "v1": + // valid + case "v2": + // valid + default: + return errors.Errorf("%s: not a supported AWS Instance Metadata Service version", v) + } + } + return nil } From e9b500daf2185110f49b2eaa4a3a0f9e17020753 Mon Sep 17 00:00:00 2001 From: Josh Hogle Date: Wed, 20 May 2020 14:43:25 -0400 Subject: [PATCH 08/20] Updated error message --- authority/provisioner/aws.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 52c8a40d..fd03d0c3 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -201,14 +201,14 @@ func (p *AWS) GetIdentityToken(subject, caURL string) (string, error) { var idoc awsInstanceIdentityDocument doc, err := p.readURL(p.config.identityURL) if err != nil { - return "", errors.Wrap(err, "error retrieving identity document, are you in an AWS VM using the proper IMDS version?") + return "", errors.Wrap(err, "error retrieving identity document:\n Are you in an AWS VM?\n Is the metadata service enabled?\n Are you using the proper metadata service version?") } if err := json.Unmarshal(doc, &idoc); err != nil { return "", errors.Wrap(err, "error unmarshaling identity document") } sig, err := p.readURL(p.config.signatureURL) if err != nil { - return "", errors.Wrap(err, "error retrieving identity document signature, are you in an AWS VM using the proper IMDS version?") + return "", errors.Wrap(err, "error retrieving identity document:\n Are you in an AWS VM?\n Is the metadata service enabled?\n Are you using the proper metadata service version?") } signature, err := base64.StdEncoding.DecodeString(string(sig)) if err != nil { From 51f16ee2e01062aa7d31b80245d8e6663fe4e28c Mon Sep 17 00:00:00 2001 From: David Cowden Date: Wed, 22 Jul 2020 16:52:06 -0700 Subject: [PATCH 09/20] aws: add tests covering metadata service versions * Add constructor tests for the aws provisioner. * Add a test to make sure the "v1" logic continues to work. By and large, v2 is the way to go. However, there are some instances of things that specifically request metadata service version 1 and so this adds minimal coverage to make sure we don't accidentally break the path should anyone need to depend on the former logic. --- authority/provisioner/aws.go | 3 + authority/provisioner/aws_test.go | 42 +++++++++++-- authority/provisioner/utils_test.go | 95 +++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 6 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 3e777e08..56a96490 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -386,6 +386,9 @@ func (p *AWS) readURL(url string) ([]byte, error) { default: return nil, fmt.Errorf("%s: not a supported AWS Instance Metadata Service version", v) } + if resp != nil { + resp.Body.Close() + } } // all versions have been exhausted and we haven't returned successfully yet so pass diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 50cdd32a..b5728de4 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -187,6 +187,31 @@ func TestAWS_GetIdentityToken(t *testing.T) { } } +func TestAWS_GetIdentityTokenV1Only(t *testing.T) { + aws, srv, err := generateAWSWithServerV1Only() + assert.FatalError(t, err) + defer srv.Close() + + subject := "foo.local" + caURL := "https://ca.smallstep.com" + u, err := url.Parse(caURL) + assert.Nil(t, err) + + token, err := aws.GetIdentityToken(subject, caURL) + assert.Nil(t, err) + + _, c, err := parseAWSToken(token) + if assert.NoError(t, err) { + assert.Equals(t, awsIssuer, c.Issuer) + 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) + assert.NoError(t, err) + } +} + func TestAWS_Init(t *testing.T) { config := Config{ Claims: globalProvisionerClaims, @@ -203,6 +228,7 @@ func TestAWS_Init(t *testing.T) { DisableCustomSANs bool DisableTrustOnFirstUse bool InstanceAge Duration + IMDSVersions []string Claims *Claims } type args struct { @@ -214,12 +240,15 @@ func TestAWS_Init(t *testing.T) { args args wantErr bool }{ - {"ok", fields{"AWS", "name", []string{"account"}, false, false, zero, nil}, args{config}, false}, - {"ok", fields{"AWS", "name", []string{"account"}, true, true, Duration{Duration: 1 * time.Minute}, nil}, args{config}, false}, - {"fail type ", fields{"", "name", []string{"account"}, false, false, zero, nil}, args{config}, true}, - {"fail name", fields{"AWS", "", []string{"account"}, false, false, zero, nil}, args{config}, true}, - {"bad instance age", fields{"AWS", "name", []string{"account"}, false, false, Duration{Duration: -1 * time.Minute}, nil}, args{config}, true}, - {"fail claims", fields{"AWS", "name", []string{"account"}, false, false, zero, 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/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}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -230,6 +259,7 @@ func TestAWS_Init(t *testing.T) { DisableCustomSANs: tt.fields.DisableCustomSANs, DisableTrustOnFirstUse: tt.fields.DisableTrustOnFirstUse, InstanceAge: tt.fields.InstanceAge, + IMDSVersions: tt.fields.IMDSVersions, Claims: tt.fields.Claims, } if err := p.Init(tt.args.config); (err != nil) != tt.wantErr { diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 84283631..52fb470a 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -495,6 +495,101 @@ func generateAWSWithServer() (*AWS, *httptest.Server, error) { return aws, srv, nil } +func generateAWSV1Only() (*AWS, error) { + name, err := randutil.Alphanumeric(10) + if err != nil { + return nil, err + } + accountID, err := randutil.Alphanumeric(10) + if err != nil { + return nil, err + } + claimer, err := NewClaimer(nil, globalProvisionerClaims) + if err != nil { + return nil, err + } + block, _ := pem.Decode([]byte(awsTestCertificate)) + if block == nil || block.Type != "CERTIFICATE" { + return nil, errors.New("error decoding AWS certificate") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing AWS certificate") + } + return &AWS{ + Type: "AWS", + Name: name, + Accounts: []string{accountID}, + Claims: &globalProvisionerClaims, + IMDSVersions: []string{"v1"}, + claimer: claimer, + config: &awsConfig{ + identityURL: awsIdentityURL, + signatureURL: awsSignatureURL, + tokenURL: awsAPITokenURL, + tokenTTL: awsAPITokenTTL, + certificate: cert, + signatureAlgorithm: awsSignatureAlgorithm, + }, + audiences: testAudiences.WithFragment("aws/" + name), + }, nil +} + +func generateAWSWithServerV1Only() (*AWS, *httptest.Server, error) { + aws, err := generateAWSV1Only() + if err != nil { + return nil, nil, err + } + block, _ := pem.Decode([]byte(awsTestKey)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return nil, nil, errors.New("error decoding AWS key") + } + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, nil, errors.Wrap(err, "error parsing AWS private key") + } + doc, err := json.MarshalIndent(awsInstanceIdentityDocument{ + AccountID: aws.Accounts[0], + Architecture: "x86_64", + AvailabilityZone: "us-west-2b", + ImageID: "image-id", + InstanceID: "instance-id", + InstanceType: "t2.micro", + PendingTime: time.Now(), + PrivateIP: "127.0.0.1", + Region: "us-west-1", + Version: "2017-09-30", + }, "", " ") + if err != nil { + return nil, nil, err + } + + sum := sha256.Sum256(doc) + signature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256) + if err != nil { + return nil, nil, errors.Wrap(err, "error signing document") + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/latest/dynamic/instance-identity/document": + w.Write(doc) + case "/latest/dynamic/instance-identity/signature": + w.Write([]byte(base64.StdEncoding.EncodeToString(signature))) + case "/bad-document": + w.Write([]byte("{}")) + case "/bad-signature": + w.Write([]byte("YmFkLXNpZ25hdHVyZQo=")) + case "/bad-json": + w.Write([]byte("{")) + default: + http.NotFound(w, r) + } + })) + aws.config.identityURL = srv.URL + "/latest/dynamic/instance-identity/document" + aws.config.signatureURL = srv.URL + "/latest/dynamic/instance-identity/signature" + return aws, srv, nil +} + func generateAzure() (*Azure, error) { name, err := randutil.Alphanumeric(10) if err != nil { From dc39eef7212ecf19f0a2c5dfd6df1f8b4443fba5 Mon Sep 17 00:00:00 2001 From: David Cowden Date: Wed, 22 Jul 2020 17:40:26 -0700 Subject: [PATCH 10/20] aws: test badIDMS functional path The existing test only covers the constructor logic. Also test the live code path that is executed when a bad IDMS version is supplied. --- authority/provisioner/aws_test.go | 20 +++++++++++++++++++- go.mod | 2 +- go.sum | 2 ++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index b5728de4..8aa01be0 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -187,7 +187,7 @@ func TestAWS_GetIdentityToken(t *testing.T) { } } -func TestAWS_GetIdentityTokenV1Only(t *testing.T) { +func TestAWS_GetIdentityToken_V1Only(t *testing.T) { aws, srv, err := generateAWSWithServerV1Only() assert.FatalError(t, err) defer srv.Close() @@ -212,6 +212,24 @@ func TestAWS_GetIdentityTokenV1Only(t *testing.T) { } } +func TestAWS_GetIdentityToken_BadIDMS(t *testing.T) { + aws, srv, err := generateAWSWithServer() + + aws.IMDSVersions = []string{"bad"} + + assert.FatalError(t, err) + defer srv.Close() + + subject := "foo.local" + caURL := "https://ca.smallstep.com" + + token, err := aws.GetIdentityToken(subject, caURL) + assert.Equals(t, token, "") + + badIDMS := errors.New("bad: not a supported AWS Instance Metadata Service version") + assert.HasSuffix(t, err.Error(), badIDMS.Error()) +} + func TestAWS_Init(t *testing.T) { config := Config{ Claims: globalProvisionerClaims, diff --git a/go.mod b/go.mod index a1b6625a..9df83f5d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 - github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 + github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/smallstep/cli v0.14.6 github.com/smallstep/nosql v0.3.0 github.com/urfave/cli v1.22.2 diff --git a/go.sum b/go.sum index 430a7451..92918917 100644 --- a/go.sum +++ b/go.sum @@ -468,6 +468,8 @@ github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3q github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 h1:kSImCuenAkXtCaBeQ1UhmzzJGRhSm8sVH7I3sHE2Qdg= github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/certificates v0.14.5/go.mod h1:zzpB8wMz967gL8FmK6zvCNB4pDVwFDKjPg1diTVc1h8= github.com/smallstep/certinfo v1.3.0/go.mod h1:1gQJekdPwPvUwFWGTi7bZELmQT09cxC9wJ0VBkBNiwU= github.com/smallstep/cli v0.14.5 h1:avA6q9h2aIbAQa/vTFV5psjJ1mg8NQliKC+RsFIC778= From 2b121efc8f9611679eed8a900c733e6dc2622e11 Mon Sep 17 00:00:00 2001 From: David Cowden Date: Wed, 22 Jul 2020 18:33:44 -0700 Subject: [PATCH 11/20] aws: test constructor with empty IDMS string array --- authority/provisioner/aws_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index 8aa01be0..f2a5d100 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -261,6 +261,7 @@ func TestAWS_Init(t *testing.T) { {"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}, From 86efe7aff0b0ed88f89543ce27666754a254b162 Mon Sep 17 00:00:00 2001 From: David Cowden Date: Wed, 22 Jul 2020 18:39:46 -0700 Subject: [PATCH 12/20] aws: use http.NoBody instead of nil It's a little more descriptive. --- authority/provisioner/aws.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index 56a96490..12dffaa8 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -403,7 +403,7 @@ func (p *AWS) readURL(url string) ([]byte, error) { func (p *AWS) readURLv1(url string) (*http.Response, error) { client := http.Client{} - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequest(http.MethodGet, url, http.NoBody) if err != nil { return nil, err } @@ -437,7 +437,7 @@ func (p *AWS) readURLv2(url string) (*http.Response, error) { } // now make the request - req, err = http.NewRequest(http.MethodGet, url, nil) + req, err = http.NewRequest(http.MethodGet, url, http.NoBody) if err != nil { return nil, err } From 3f844c5e2320fc5259275ef1c68753e4630fcd27 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 28 Jul 2020 12:00:07 -0700 Subject: [PATCH 13/20] Update the way SubjectKeyId is calculated, and more ... - swith lint to first in line for `make all` - update tests to conform with new subjectkeyid --- Makefile | 2 +- authority/tls_test.go | 40 +++++++++++++++++++++++++++++++--------- ca/ca_test.go | 37 +++++++++++++++++++++++++++++++------ go.mod | 2 +- go.sum | 4 ++++ 5 files changed, 68 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index b4632eef..870ad000 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*") GOOS_OVERRIDE ?= OUTPUT_ROOT=output/ -all: build test lint +all: lint build test .PHONY: all diff --git a/authority/tls_test.go b/authority/tls_test.go index e8a0497b..2e44c414 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -357,10 +357,9 @@ ZYtQ9Ot36qc= []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"}) - pubBytes, err := x509.MarshalPKIXPublicKey(pub) + kid, err := generateSubjectKeyID(pub) assert.FatalError(t, err) - hash := sha1.Sum(pubBytes) - assert.Equals(t, leaf.SubjectKeyId, hash[:]) + assert.Equals(t, leaf.SubjectKeyId, kid) assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId) @@ -397,6 +396,31 @@ ZYtQ9Ot36qc= } } +// subjectPublicKeyInfo is a PKIX public key structure defined in RFC 5280. +type subjectPublicKeyInfo struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString +} + +// generateSubjectKeyID generates the key identifier according the the RFC 5280 +// section 4.2.1.2. +// +// The keyIdentifier is composed of the 160-bit SHA-1 hash of the value of the +// BIT STRING subjectPublicKey (excluding the tag, length, and number of unused +// bits). +func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { + b, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, errors.Wrap(err, "error marshaling public key") + } + var info subjectPublicKeyInfo + if _, err = asn1.Unmarshal(b, &info); err != nil { + return nil, errors.Wrap(err, "error unmarshaling public key") + } + hash := sha1.Sum(info.SubjectPublicKey.Bytes) + return hash[:], nil +} + func TestAuthority_Renew(t *testing.T) { pub, _, err := keys.GenerateDefaultKeyPair() assert.FatalError(t, err) @@ -554,10 +578,9 @@ func TestAuthority_Renew(t *testing.T) { // Test Public Key and SubjectKeyId assert.Equals(t, leaf.PublicKey, cert.PublicKey) - pubBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) + kid, err := generateSubjectKeyID(cert.PublicKey) assert.FatalError(t, err) - hash := sha1.Sum(pubBytes) - assert.Equals(t, leaf.SubjectKeyId, hash[:]) + assert.Equals(t, leaf.SubjectKeyId, kid) assert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId) // We did not change the intermediate before renewing. @@ -791,10 +814,9 @@ func TestAuthority_Rekey(t *testing.T) { } assert.Equals(t, leaf.PublicKey, expectedPK) - pubBytes, err := x509.MarshalPKIXPublicKey(expectedPK) + kid, err := generateSubjectKeyID(expectedPK) assert.FatalError(t, err) - hash := sha1.Sum(pubBytes) - assert.Equals(t, leaf.SubjectKeyId, hash[:]) + assert.Equals(t, leaf.SubjectKeyId, kid) if tc.pk == nil { assert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId) } diff --git a/ca/ca_test.go b/ca/ca_test.go index 1aaed206..568e7917 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -2,11 +2,13 @@ package ca import ( "bytes" + "crypto" "crypto/rand" "crypto/sha1" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/json" "encoding/pem" "fmt" @@ -32,6 +34,31 @@ import ( "gopkg.in/square/go-jose.v2/jwt" ) +// subjectPublicKeyInfo is a PKIX public key structure defined in RFC 5280. +type subjectPublicKeyInfo struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString +} + +// generateSubjectKeyID generates the key identifier according the the RFC 5280 +// section 4.2.1.2. +// +// The keyIdentifier is composed of the 160-bit SHA-1 hash of the value of the +// BIT STRING subjectPublicKey (excluding the tag, length, and number of unused +// bits). +func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { + b, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, errors.Wrap(err, "error marshaling public key") + } + var info subjectPublicKeyInfo + if _, err = asn1.Unmarshal(b, &info); err != nil { + return nil, errors.Wrap(err, "error unmarshaling public key") + } + hash := sha1.Sum(info.SubjectPublicKey.Bytes) + return hash[:], nil +} + type ClosingBuffer struct { *bytes.Buffer } @@ -299,10 +326,9 @@ ZEp7knvU2psWRw== []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"}) - pubBytes, err := x509.MarshalPKIXPublicKey(pub) + kid, err := generateSubjectKeyID(pub) assert.FatalError(t, err) - hash := sha1.Sum(pubBytes) - assert.Equals(t, leaf.SubjectKeyId, hash[:]) + assert.Equals(t, leaf.SubjectKeyId, kid) assert.Equals(t, leaf.AuthorityKeyId, intermediateIdentity.Crt.SubjectKeyId) @@ -641,10 +667,9 @@ func TestCARenew(t *testing.T) { []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) assert.Equals(t, leaf.DNSNames, []string{"funk"}) - pubBytes, err := x509.MarshalPKIXPublicKey(pub) + kid, err := generateSubjectKeyID(pub) assert.FatalError(t, err) - hash := sha1.Sum(pubBytes) - assert.Equals(t, leaf.SubjectKeyId, hash[:]) + assert.Equals(t, leaf.SubjectKeyId, kid) assert.Equals(t, leaf.AuthorityKeyId, intermediateIdentity.Crt.SubjectKeyId) diff --git a/go.mod b/go.mod index 9df83f5d..96dfd661 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 - github.com/smallstep/cli v0.14.6 + github.com/smallstep/cli v0.14.7-rc.1.0.20200727165646-eb4e97335f2d github.com/smallstep/nosql v0.3.0 github.com/urfave/cli v1.22.2 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 diff --git a/go.sum b/go.sum index 92918917..264f7505 100644 --- a/go.sum +++ b/go.sum @@ -476,9 +476,12 @@ github.com/smallstep/cli v0.14.5 h1:avA6q9h2aIbAQa/vTFV5psjJ1mg8NQliKC+RsFIC778= github.com/smallstep/cli v0.14.5/go.mod h1:mRFuqC3cGwQESBGJvog4o76jZZZ7bMjkE+hAnq2QyR8= github.com/smallstep/cli v0.14.6 h1:xc9rawDKB70Vgvg10gfQAh9EpDWS3k1O002J5bApqUk= github.com/smallstep/cli v0.14.6/go.mod h1:Gs9mXwk5tIBlSISt1tvCAMso7eEAiJRyNgR/6JsoojI= +github.com/smallstep/cli v0.14.7-rc.1.0.20200727165646-eb4e97335f2d h1:bDnvzyEXzAMO5in8QHRrQCEUopiXjFVKHiR8c0m7Iww= +github.com/smallstep/cli v0.14.7-rc.1.0.20200727165646-eb4e97335f2d/go.mod h1:7aWHk7WwJMpEP4PYyav86FMpaI9vuA0uJRliUAqCwxg= github.com/smallstep/nosql v0.3.0 h1:V1X5vfDsDt89499h3jZFUlR4VnnsYYs5tXaQZ0w8z5U= github.com/smallstep/nosql v0.3.0/go.mod h1:QG7gNOpidifn99MjZaiNbm7HPesIyBd97F/OfacNz8Q= github.com/smallstep/truststore v0.9.3/go.mod h1:PRSkpRIhAYBK/KLWkHNgRdYgzWMEy45bN7PSJCfKKGE= +github.com/smallstep/truststore v0.9.6/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= github.com/smallstep/zcrypto v0.0.0-20200203191936-fbc32cf76bce/go.mod h1:+F24VU3UCxfVFvvqgm5jNUFQOm/L6ed13ImwWGFgg/g= github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -805,6 +808,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= From 3fb116f1b4e90eb561d6e94b3548fdc27cadab1b Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 31 Jul 2020 10:32:08 -0700 Subject: [PATCH 14/20] Add SSHPOP default provisioner if SSH enabled during init --- pki/pki.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pki/pki.go b/pki/pki.go index d52f247e..ec105e3b 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -446,9 +446,19 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { HostKey: p.sshHostKey, UserKey: p.sshUserKey, } + // Enable SSH authorization for default JWK provisioner prov.Claims = &provisioner.Claims{ EnableSSHCA: &enableSSHCA, } + // Add default SSHPOP provisioner + sshpop := &provisioner.SSHPOP{ + Type: "SSHPOP", + Name: "sshpop", + Claims: &provisioner.Claims{ + EnableSSHCA: &enableSSHCA, + }, + } + config.AuthorityConfig.Provisioners = append(config.AuthorityConfig.Provisioners, sshpop) } // Apply configuration modifiers From 9e9808fe3d9b4da618e38689f9d61a1071c4a864 Mon Sep 17 00:00:00 2001 From: max furman Date: Sun, 2 Aug 2020 13:25:21 -0700 Subject: [PATCH 15/20] introduce docker-buildx --- Makefile | 79 ++++++-------------------------------------------- make/docker.mk | 67 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 70 deletions(-) create mode 100644 make/docker.mk diff --git a/Makefile b/Makefile index 870ad000..74063957 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,11 @@ VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev") # If we are not in an active git dir then try reading the version from .VERSION. # .VERSION contains a slug populated by `git archive`. VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION)) + ifeq ($(TRAVIS_BRANCH),master) PUSHTYPE := master + else +PUSHTYPE := branch + endif endif VERSION := $(shell echo $(VERSION) | sed 's/^v//') @@ -57,6 +61,8 @@ $(info VERSION is $(VERSION)) $(info PUSHTYPE is $(PUSHTYPE)) endif +include make/docker.mk + ######################################### # Build ######################################### @@ -167,76 +173,6 @@ run: .PHONY: run -######################################### -# Building Docker Image -# -# Builds a dockerfile for step by building a linux version of the step-cli and -# then copying the specific binary when building the container. -# -# This ensures the container is as small as possible without having to deal -# with getting access to private repositories inside the container during build -# time. -######################################### - -# XXX We put the output for the build in 'output' so we don't mess with how we -# do rule overriding from the base Makefile (if you name it 'build' it messes up -# the wildcarding). -DOCKER_OUTPUT=$(OUTPUT_ROOT)docker/ - -DOCKER_MAKE=V=$V GOOS_OVERRIDE='GOOS=linux GOARCH=amd64' PREFIX=$(1) make $(1)bin/$(2) -DOCKER_BUILD=$Q docker build -t smallstep/$(1):latest -f docker/$(2) --build-arg BINPATH=$(DOCKER_OUTPUT)bin/$(1) . - -docker: docker-make docker/Dockerfile.step-ca - $(call DOCKER_BUILD,step-ca,Dockerfile.step-ca) - -docker-make: - mkdir -p $(DOCKER_OUTPUT) - $(call DOCKER_MAKE,$(DOCKER_OUTPUT),step-ca) - -.PHONY: docker docker-make - -################################################# -# Releasing Docker Images -# -# Using the docker build infrastructure, this section is responsible for -# logging into docker hub and pushing the built docker containers up with the -# appropriate tags. -################################################# - -DOCKER_TAG=docker tag smallstep/$(1):latest smallstep/$(1):$(2) -DOCKER_PUSH=docker push smallstep/$(1):$(2) - -docker-tag: - $(call DOCKER_TAG,step-ca,$(VERSION)) - -docker-push-tag: docker-tag - $(call DOCKER_PUSH,step-ca,$(VERSION)) - -docker-push-tag-latest: - $(call DOCKER_PUSH,step-ca,latest) - -# Rely on DOCKER_USERNAME and DOCKER_PASSWORD being set inside the CI or -# equivalent environment -docker-login: - $Q docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)" - -.PHONY: docker-login docker-tag docker-push-tag docker-push-tag-latest - -################################################# -# Targets for pushing the docker images -################################################# - -# For all builds we build the docker container -docker-master: docker - -# For all builds with a release candidate tag -docker-release-candidate: docker-master docker-login docker-push-tag - -# For all builds with a release tag -docker-release: docker-release-candidate docker-push-tag-latest - -.PHONY: docker-master docker-release-candidate docker-release - ######################################### # Debian ######################################### @@ -323,6 +259,9 @@ artifacts-tag: artifacts-linux-tag artifacts-darwin-tag artifacts-archive-tag # Targets for creating step artifacts ################################################# +# For all builds that are not tagged and not on the master branch +artifacts-branch: + # For all builds that are not tagged artifacts-master: diff --git a/make/docker.mk b/make/docker.mk new file mode 100644 index 00000000..9a9e3c6d --- /dev/null +++ b/make/docker.mk @@ -0,0 +1,67 @@ +######################################### +# Building Docker Image +# +# This uses a multi-stage build file. The first stage is a builder (that might +# be large in size). After the build has succeeded, the statically linked +# binary is copied to a new image that is optimized for size. +######################################### + +docker-prepare: + # Ensure, we can build for ARM architecture + [ -f /proc/sys/fs/binfmt_misc/qemu-arm ] || docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64 + + # Register buildx builder + mkdir -p $$HOME/.docker/cli-plugins + + wget -O $$HOME/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.3.1/buildx-v0.3.1.linux-amd64 + chmod +x $$HOME/.docker/cli-plugins/docker-buildx + + $$HOME/.docker/cli-plugins/docker-buildx create --name mybuilder --platform amd64 --platform arm || true + $$HOME/.docker/cli-plugins/docker-buildx use mybuilder + +.PHONY: docker-prepare + +################################################# +# Releasing Docker Images +# +# Using the docker build infrastructure, this section is responsible for +# logging into docker hub. +################################################# + +# Rely on DOCKER_USERNAME and DOCKER_PASSWORD being set inside the CI or +# equivalent environment +docker-login: + $Q docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)" + +.PHONY: docker-login + +################################################# +# Targets for different type of builds +################################################# + +DOCKER_IMAGE_NAME = smallstep/step-ca +PLATFORMS = --platform amd64 --platform 386 --platform arm --platform arm64 + +define DOCKER_BUILDX + # $(1) -- Image Tag + # $(2) -- Push (empty is no push | --push will push to dockerhub) + $$HOME/.docker/cli-plugins/docker-buildx build . --progress plain -t $(DOCKER_IMAGE_NAME):$(1) -f docker/Dockerfile.step-ca $(PLATFORMS) $(2) +endef + +# For non-master builds don't build the docker containers. +docker-branch: + +# For master builds create the docker containers but don't push them. +docker-master: docker-prepare + $(call DOCKER_BUILDX,latest,) + +# For all builds with a release candidate tag build and push the containers. +docker-release-candidate: docker-prepare docker-login + $(call DOCKER_BUILDX,$(VERSION),--push) + +# For all builds with a release tag build and push the containers. +docker-release: docker-prepare docker-login + $(call DOCKER_BUILDX,latest,--push) + $(call DOCKER_BUILDX,$(VERSION),--push) + +.PHONY: docker-branch docker-master docker-release-candidate docker-release From 652377dbf6876a44aa34e0dcc893188feb259212 Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 3 Aug 2020 09:23:29 -0700 Subject: [PATCH 16/20] Convert '-' to '~' in deb package name --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 870ad000..84b6b052 100644 --- a/Makefile +++ b/Makefile @@ -50,10 +50,12 @@ PUSHTYPE := master endif VERSION := $(shell echo $(VERSION) | sed 's/^v//') +DEB_VERSION := $(shell echo $(VERSION) | sed 's/-/~/g') ifdef V $(info TRAVIS_TAG is $(TRAVIS_TAG)) $(info VERSION is $(VERSION)) +$(info DEB_VERSION is $(DEB_VERSION)) $(info PUSHTYPE is $(PUSHTYPE)) endif @@ -242,7 +244,7 @@ docker-release: docker-release-candidate docker-push-tag-latest ######################################### changelog: - $Q echo "step-certificates ($(VERSION)) unstable; urgency=medium" > debian/changelog + $Q echo "step-certificates ($(DEB_VERSION)) unstable; urgency=medium" > debian/changelog $Q echo >> debian/changelog $Q echo " * See https://github.com/smallstep/certificates/releases" >> debian/changelog $Q echo >> debian/changelog From 476bca371759b12465451075a636b3d8d7006d6b Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 3 Aug 2020 20:45:51 -0700 Subject: [PATCH 17/20] Add make docker-dev building and testing locally. --- make/docker.mk | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/make/docker.mk b/make/docker.mk index 9a9e3c6d..da41fd60 100644 --- a/make/docker.mk +++ b/make/docker.mk @@ -65,3 +65,18 @@ docker-release: docker-prepare docker-login $(call DOCKER_BUILDX,$(VERSION),--push) .PHONY: docker-branch docker-master docker-release-candidate docker-release + +# XXX We put the output for the build in 'output' so we don't mess with how we +# do rule overriding from the base Makefile (if you name it 'build' it messes up +# the wildcarding). +DOCKER_OUTPUT=$(OUTPUT_ROOT)docker/ + +DOCKER_MAKE=V=$V GOOS_OVERRIDE='GOOS=linux GOARCH=amd64' PREFIX=$(1) make $(1)bin/$(BINNAME) +DOCKER_BUILD=$Q docker build -t $(DOCKER_IMAGE_NAME):latest -f docker/Dockerfile.step-ca --build-arg BINPATH=$(DOCKER_OUTPUT)bin/$(BINNAME) . + +docker-dev: docker/Dockerfile.step-ca + mkdir -p $(DOCKER_OUTPUT) + $(call DOCKER_MAKE,$(DOCKER_OUTPUT),step-ca) + $(call DOCKER_BUILD) + +.PHONY: docker-dev From e8c5a3b32055b086b9b01be5beb219c291214d48 Mon Sep 17 00:00:00 2001 From: max furman Date: Fri, 7 Aug 2020 13:48:35 -0700 Subject: [PATCH 18/20] Document concurrency limitations in ACME server - in concurrency / HA section --- docs/GETTING_STARTED.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 0e010019..84e968ab 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -718,6 +718,11 @@ A few things to consider / implement when running multiple instances of `step-ca * Use `MySQL` DB: The default `Badger` DB cannot be read / written by more than one process simultaneously. The only supported DB that can support multiple instances is `MySQL`. See the [database documentation][4] for guidance on configuring `MySQL`. + * The ACME server has known concurrency limitations when using the same account to + manage multiple orders. The recommended temporary workaround is to generate + an ephemeral account keypair for each new ACME order, or to ensure that ACME + orders owned by the same account are managed serially. The issue tracking + this limitation can be found [here](https://github.com/smallstep/certificates/issues/341). * Synchronize `ca.json` across instances: `step-ca` reads all of it's configuration (and all of the provisioner configuration) from the `ca.json` file From 55bf5a4526bf836dc284f2b16906e851939190d0 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 12 Aug 2020 15:50:45 -0700 Subject: [PATCH 19/20] Add cert logging for acme/certificate api --- acme/api/handler.go | 14 ++++++++++++++ acme/api/handler_test.go | 39 ++++++++++++++++++++++++++++++++++++++- api/api.go | 8 ++++++-- api/rekey.go | 2 +- api/renew.go | 2 +- api/revoke.go | 2 +- api/sign.go | 2 +- 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/acme/api/handler.go b/acme/api/handler.go index dd30c7c1..921e614e 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -2,6 +2,8 @@ package api import ( "context" + "crypto/x509" + "encoding/pem" "fmt" "net/http" @@ -162,6 +164,18 @@ func (h *Handler) GetCertificate(w http.ResponseWriter, r *http.Request) { return } + block, _ := pem.Decode(certBytes) + if block == nil { + api.WriteError(w, acme.ServerInternalErr(errors.New("failed to decode any certificates from generated certBytes"))) + return + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + api.WriteError(w, acme.Wrap(err, "failed to parse generated leaf certificate")) + return + } + + api.LogCertificate(w, cert) w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8") w.Write(certBytes) } diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index f8bac96c..e3db69b7 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -526,6 +526,43 @@ func TestHandlerGetCertificate(t *testing.T) { problem: acme.ServerInternalErr(errors.New("force")), } }, + "fail/decode-leaf-for-loggger": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), acme.AccContextKey, acc) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + auth: &mockAcmeAuthority{ + getCertificate: func(accID, id string) ([]byte, error) { + assert.Equals(t, accID, acc.ID) + assert.Equals(t, id, certID) + return []byte("foo"), nil + }, + }, + ctx: ctx, + statusCode: 500, + problem: acme.ServerInternalErr(errors.New("failed to decode any certificates from generated certBytes")), + } + }, + "fail/parse-x509-leaf-for-logger": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), acme.AccContextKey, acc) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + auth: &mockAcmeAuthority{ + getCertificate: func(accID, id string) ([]byte, error) { + assert.Equals(t, accID, acc.ID) + assert.Equals(t, id, certID) + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: []byte("foo"), + }), nil + }, + }, + ctx: ctx, + statusCode: 500, + problem: acme.ServerInternalErr(errors.New("failed to parse generated leaf certificate")), + } + }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := context.WithValue(context.Background(), acme.AccContextKey, acc) @@ -565,7 +602,7 @@ func TestHandlerGetCertificate(t *testing.T) { prob := tc.problem.ToACME() assert.Equals(t, ae.Type, prob.Type) - assert.Equals(t, ae.Detail, prob.Detail) + assert.HasPrefix(t, ae.Detail, prob.Detail) assert.Equals(t, ae.Identifier, prob.Identifier) assert.Equals(t, ae.Subproblems, prob.Subproblems) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) diff --git a/api/api.go b/api/api.go index f83a2354..23856e09 100644 --- a/api/api.go +++ b/api/api.go @@ -395,7 +395,8 @@ func logOtt(w http.ResponseWriter, token string) { } } -func logCertificate(w http.ResponseWriter, cert *x509.Certificate) { +// LogCertificate add certificate fields to the log message. +func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { if rl, ok := w.(logging.ResponseLogger); ok { m := map[string]interface{}{ "serial": cert.SerialNumber, @@ -413,7 +414,10 @@ func logCertificate(w http.ResponseWriter, cert *x509.Certificate) { if err != nil || len(rest) > 0 { break } - m["provisioner"] = fmt.Sprintf("%s (%s)", val.Name, val.CredentialID) + m["provisioner"] = fmt.Sprintf("%s", val.Name) + if len(val.CredentialID) > 0 { + m["provisioner"] = fmt.Sprintf("%s (%s)", m["provisioner"], val.CredentialID) + } break } } diff --git a/api/rekey.go b/api/rekey.go index 2d24dbb8..c0d88e55 100644 --- a/api/rekey.go +++ b/api/rekey.go @@ -54,7 +54,7 @@ func (h *caHandler) Rekey(w http.ResponseWriter, r *http.Request) { caPEM = certChainPEM[1] } - logCertificate(w, certChain[0]) + LogCertificate(w, certChain[0]) JSONStatus(w, &SignResponse{ ServerPEM: certChainPEM[0], CaPEM: caPEM, diff --git a/api/renew.go b/api/renew.go index bf32518b..74ef2034 100644 --- a/api/renew.go +++ b/api/renew.go @@ -25,7 +25,7 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { caPEM = certChainPEM[1] } - logCertificate(w, certChain[0]) + LogCertificate(w, certChain[0]) JSONStatus(w, &SignResponse{ ServerPEM: certChainPEM[0], CaPEM: caPEM, diff --git a/api/revoke.go b/api/revoke.go index 547ed366..21c3154c 100644 --- a/api/revoke.go +++ b/api/revoke.go @@ -91,7 +91,7 @@ func (h *caHandler) Revoke(w http.ResponseWriter, r *http.Request) { // TODO: should probably be checking if the certificate was revoked here. // Will need to thread that request down to the authority, so will need // to add API for that. - logCertificate(w, opts.Crt) + LogCertificate(w, opts.Crt) opts.MTLS = true } diff --git a/api/sign.go b/api/sign.go index f30b0b4b..7826f47b 100644 --- a/api/sign.go +++ b/api/sign.go @@ -79,7 +79,7 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { if len(certChainPEM) > 1 { caPEM = certChainPEM[1] } - logCertificate(w, certChain[0]) + LogCertificate(w, certChain[0]) JSONStatus(w, &SignResponse{ ServerPEM: certChainPEM[0], CaPEM: caPEM, From 8e3481a8ef7bcc38b459aa9dcce6d00458871f1e Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 12 Aug 2020 16:35:38 -0700 Subject: [PATCH 20/20] [logger map] small optimization Rather than doing two key writes and one lookup, just write once. --- api/api.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 23856e09..6e0b08eb 100644 --- a/api/api.go +++ b/api/api.go @@ -414,9 +414,10 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { if err != nil || len(rest) > 0 { break } - m["provisioner"] = fmt.Sprintf("%s", val.Name) if len(val.CredentialID) > 0 { - m["provisioner"] = fmt.Sprintf("%s (%s)", m["provisioner"], val.CredentialID) + m["provisioner"] = fmt.Sprintf("%s (%s)", val.Name, val.CredentialID) + } else { + m["provisioner"] = fmt.Sprintf("%s", val.Name) } break }