diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4a273c46 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: true +contact_links: + - name: Ask on Discord + url: https://discord.gg/7xgjhVAg6g + about: You can ask for help here! + - name: Want to contribute to step certificates? + url: https://github.com/smallstep/certificates/blob/master/docs/CONTRIBUTING.md + about: Be sure to read contributing guidelines! + diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 28eec406..3a6ffc94 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -1,5 +1,5 @@ --- -name: Certificates Enhancement +name: Enhancement about: Suggest an enhancement to step certificates title: '' labels: enhancement, needs triage diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 72b01a92..bf5b4bc0 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,14 +1,12 @@ -name: labeler +name: Pull Request Labeler on: - pull_request: - branches: - - master + pull_request_target jobs: label: runs-on: ubuntu-latest steps: - - uses: actions/labeler@v3 + - uses: actions/labeler@v3.0.2 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - configuration-path: .github/labeler.yml + diff --git a/README.md b/README.md index f0649175..64458929 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,14 @@ You can use it to: Whatever your use case, `step-ca` is easy to use and hard to misuse, thanks to [safe, sane defaults](https://smallstep.com/docs/step-ca/certificate-authority-server-production#sane-cryptographic-defaults). -**Questions? Find us in [Discussions](https://github.com/smallstep/certificates/discussions).** +--- + +**Don't want to run your own CA?** +To get up and running quickly, or as an alternative to running your own `step-ca` server, consider creating a [free hosted smallstep Certificate Manager authority](https://info.smallstep.com/certificate-manager-early-access-mvp/). + +--- + +**Questions? Find us in [Discussions](https://github.com/smallstep/certificates/discussions) or [Join our Discord](https://bit.ly/step-discord).** [Website](https://smallstep.com/certificates) | [Documentation](https://smallstep.com/docs) | @@ -27,7 +34,6 @@ Whatever your use case, `step-ca` is easy to use and hard to misuse, thanks to [ [Contributor's Guide](./docs/CONTRIBUTING.md) [![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases/latest) -[![CA Image](https://images.microbadger.com/badges/image/smallstep/step-ca.svg)](https://microbadger.com/images/smallstep/step-ca) [![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates) [![Build Status](https://travis-ci.com/smallstep/certificates.svg?branch=master)](https://travis-ci.com/smallstep/certificates) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) @@ -58,10 +64,10 @@ You can issue certificates in exchange for: - ID tokens from Okta, GSuite, Azure AD, Auth0. - ID tokens from an OAuth OIDC service that you host, like [Keycloak](https://www.keycloak.org/) or [Dex](https://github.com/dexidp/dex) - [Cloud instance identity documents](https://smallstep.com/blog/embarrassingly-easy-certificates-on-aws-azure-gcp/), for VMs on AWS, GCP, and Azure -- [Single-use, short-lived JWK tokens]() issued by your CD tool — Puppet, Chef, Ansible, Terraform, etc. +- [Single-use, short-lived JWK tokens](https://smallstep.com/docs/step-ca/provisioners#jwk) issued by your CD tool — Puppet, Chef, Ansible, Terraform, etc. - A trusted X.509 certificate (X5C provisioner) - Expiring SSH host certificates needing rotation (the SSHPOP provisioner) -- Learn more in our [provisioner documentation](https://smallstep.com/docs/step-ca/configuration#jwk) +- Learn more in our [provisioner documentation](https://smallstep.com/docs/step-ca/provisioners) ### 🏔 Your own private ACME server diff --git a/acme/challenge.go b/acme/challenge.go index 1d5f0ec9..70c52578 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -10,11 +10,13 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" "io/ioutil" "net" "net/http" "net/url" + "reflect" "strings" "time" @@ -114,6 +116,17 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb return nil } +func tlsAlert(err error) uint8 { + var opErr *net.OpError + if errors.As(err, &opErr) { + v := reflect.ValueOf(opErr.Err) + if v.Kind() == reflect.Uint8 { + return uint8(v.Uint()) + } + } + return 0 +} + func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { config := &tls.Config{ NextProtos: []string{"acme-tls/1"}, @@ -129,6 +142,14 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON conn, err := vo.TLSDial("tcp", hostPort, config) if err != nil { + // With Go 1.17+ tls.Dial fails if there's no overlap between configured + // client and server protocols. When this happens the connection is + // closed with the error no_application_protocol(120) as required by + // RFC7301. See https://golang.org/doc/go1.17#ALPN + if tlsAlert(err) == 120 { + return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, + "cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge")) + } return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err, "error doing TLS dial for %s", hostPort)) } diff --git a/acme/challenge_test.go b/acme/challenge_test.go index bb9a2507..97c5e4cd 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -1395,7 +1395,7 @@ func TestTLSALPN01Validate(t *testing.T) { assert.Equals(t, updch.Type, ch.Type) assert.Equals(t, updch.Value, ch.Value) - err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: tls: DialWithDialer timed out", ch.Value) + err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443:", ch.Value) assert.HasPrefix(t, updch.Error.Err.Error(), err.Err.Error()) assert.Equals(t, updch.Error.Type, err.Type) diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go index ba3934af..0c6bf795 100644 --- a/acme/db/nosql/order.go +++ b/acme/db/nosql/order.go @@ -124,10 +124,8 @@ func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, addOids ...st ordersByAccountMux.Lock() defer ordersByAccountMux.Unlock() + var oldOids []string b, err := db.db.Get(ordersByAccountIDTable, []byte(accID)) - var ( - oldOids []string - ) if err != nil { if !nosql.IsErrNotFound(err) { return nil, errors.Wrapf(err, "error loading orderIDs for account %s", accID) diff --git a/acme/db/nosql/order_test.go b/acme/db/nosql/order_test.go index 7248700f..8882fd82 100644 --- a/acme/db/nosql/order_test.go +++ b/acme/db/nosql/order_test.go @@ -12,6 +12,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" + "github.com/smallstep/nosql/database" nosqldb "github.com/smallstep/nosql/database" ) @@ -710,6 +711,34 @@ func TestDB_updateAddOrderIDs(t *testing.T) { err: errors.Errorf("error saving orderIDs index for account %s", accID), } }, + "ok/no-old": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(ordersByAccountIDTable): + return nil, database.ErrNotFound + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(ordersByAccountIDTable): + assert.Equals(t, key, []byte(accID)) + assert.Equals(t, old, nil) + assert.Equals(t, nu, nil) + return nil, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + res: []string{}, + } + }, "ok/all-old-not-pending": func(t *testing.T) test { oldOids := []string{"foo", "bar"} bOldOids, err := json.Marshal(oldOids) diff --git a/api/api.go b/api/api.go index 21950ccd..5be9ecc1 100644 --- a/api/api.go +++ b/api/api.go @@ -400,7 +400,7 @@ func logOtt(w http.ResponseWriter, token string) { func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { if rl, ok := w.(logging.ResponseLogger); ok { m := map[string]interface{}{ - "serial": cert.SerialNumber, + "serial": cert.SerialNumber.String(), "subject": cert.Subject.CommonName, "issuer": cert.Issuer.CommonName, "valid-from": cert.NotBefore.Format(time.RFC3339), @@ -418,7 +418,7 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { if len(val.CredentialID) > 0 { m["provisioner"] = fmt.Sprintf("%s (%s)", val.Name, val.CredentialID) } else { - m["provisioner"] = val.Name + m["provisioner"] = string(val.Name) } break } diff --git a/authority/authorize.go b/authority/authorize.go index 8555db9b..816699f7 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -174,6 +174,9 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc } // UseToken stores the token to protect against reuse. +// +// This method currently ignores any error coming from the GetTokenID, but it +// should specifically ignore the error provisioner.ErrAllowTokenReuse. func (a *Authority) UseToken(token string, prov provisioner.Interface) error { if reuseKey, err := prov.GetTokenID(token); err == nil { if reuseKey == "" { diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 230f246f..fee50658 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -131,9 +131,10 @@ func (p *Azure) GetTokenID(token string) (string, error) { return "", errors.Wrap(err, "error verifying claims") } - // If TOFU is disabled create return the token kid + // If TOFU is disabled then allow token re-use. Azure caches the token for + // 24h and without allowing the re-use we cannot use it twice. if p.DisableTrustOnFirstUse { - return claims.ID, nil + return "", ErrAllowTokenReuse } sum := sha256.Sum256([]byte(claims.XMSMirID)) diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index f21a5676..8033d345 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -72,7 +72,7 @@ func TestAzure_GetTokenID(t *testing.T) { wantErr bool }{ {"ok", p1, args{t1}, w1, false}, - {"ok no TOFU", p2, args{t2}, "the-jti", false}, + {"ok no TOFU", p2, args{t2}, "", true}, {"fail token", p1, args{"bad-token"}, "", true}, {"fail claims", p1, args{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey.fooo"}, "", true}, } diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 83cc6946..652cb888 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/json" + stderrors "errors" "net/url" "regexp" "strings" @@ -32,6 +33,17 @@ type Interface interface { AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) } +// ErrAllowTokenReuse is an error that is returned by provisioners that allows +// the reuse of tokens. +// +// This is, for example, returned by the Azure provisioner when +// DisableTrustOnFirstUse is set to true. Azure caches tokens for up to 24hr and +// has no mechanism for getting a different token - this can be an issue when +// rebooting a VM. In contrast, AWS and GCP have facilities for requesting a new +// token. Therefore, for the Azure provisioner we are enabling token reuse, with +// the understanding that we are not following security best practices +var ErrAllowTokenReuse = stderrors.New("allow token reuse") + // Audiences stores all supported audiences by request type. type Audiences struct { Sign []string diff --git a/authority/tls.go b/authority/tls.go index bba0243a..b434be55 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -383,7 +383,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } rci.ProvisionerID = p.GetID() rci.TokenID, err = p.GetTokenID(revokeOpts.OTT) - if err != nil { + if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) { return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke; could not get ID for token") } diff --git a/docker/Dockerfile.step-ca b/docker/Dockerfile.step-ca index 4a1908d6..9363b6ae 100644 --- a/docker/Dockerfile.step-ca +++ b/docker/Dockerfile.step-ca @@ -24,4 +24,7 @@ VOLUME ["/home/step"] STOPSIGNAL SIGTERM HEALTHCHECK CMD step ca health 2>/dev/null | grep "^ok" >/dev/null +COPY docker/entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] CMD exec /usr/local/bin/step-ca --password-file $PWDPATH $CONFIGPATH diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..1f48c028 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -eo pipefail + +# Paraphrased from: +# https://github.com/influxdata/influxdata-docker/blob/0d341f18067c4652dfa8df7dcb24d69bf707363d/influxdb/2.0/entrypoint.sh +# (a repo with no LICENSE.md) + +export STEPPATH=$(step path) + +# List of env vars required for step ca init +declare -ra REQUIRED_INIT_VARS=(DOCKER_STEPCA_INIT_NAME DOCKER_STEPCA_INIT_DNS_NAMES) + +# Ensure all env vars required to run step ca init are set. +function init_if_possible () { + local missing_vars=0 + for var in "${REQUIRED_INIT_VARS[@]}"; do + if [ -z "${!var}" ]; then + missing_vars=1 + fi + done + if [ ${missing_vars} = 1 ]; then + >&2 echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + else + step_ca_init "${@}" + fi +} + +function generate_password () { + set +o pipefail + < /dev/urandom tr -dc A-Za-z0-9 | head -c40 + echo + set -o pipefail +} + +# Initialize a CA if not already initialized +function step_ca_init () { + local -a setup_args=( + --name "${DOCKER_STEPCA_INIT_NAME}" + --dns "${DOCKER_STEPCA_INIT_DNS_NAMES}" + --provisioner "${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}" + --password-file "${STEPPATH}/password" + --address ":9000" + ) + if [ -n "${DOCKER_STEPCA_INIT_PASSWORD}" ]; then + echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/password" + else + generate_password > "${STEPPATH}/password" + fi + if [ -n "${DOCKER_STEPCA_INIT_SSH}" ]; then + setup_args=("${setup_args[@]}" --ssh) + fi + step ca init "${setup_args[@]}" + mv $STEPPATH/password $PWDPATH +} + +if [ ! -f "${STEPPATH}/config/ca.json" ]; then + init_if_possible +fi + +exec "${@}" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 93749026..35f75159 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,12 +7,20 @@ to manage issues, etc. ## Table of Contents -* [Building From Source](#building-from-source) -* [Asking Support Questions](#asking-support-questions) -* [Reporting Issues](#reporting-issues) -* [Submitting Patches](#submitting-patches) - * [Code Contribution Guidelines](#code-contribution-guidelines) - * [Git Commit Message Guidelines](#git-commit-message-guidelines) +- [Contributing to `step certificates`](#contributing-to-step-certificates) + - [Table of Contents](#table-of-contents) + - [Building From Source](#building-from-source) + - [Build a standard `step-ca`](#build-a-standard-step-ca) + - [Build `step-ca` using CGO](#build-step-ca-using-cgo) + - [The CGO build enables PKCS #11 and YubiKey PIV support](#the-cgo-build-enables-pkcs-11-and-yubikey-piv-support) + - [1. Install PCSC support](#1-install-pcsc-support) + - [2. Build `step-ca`](#2-build-step-ca) + - [Asking Support Questions](#asking-support-questions) + - [Reporting Issues](#reporting-issues) + - [Code Contribution](#code-contribution) + - [Submitting Patches](#submitting-patches) + - [Code Contribution Guidelines](#code-contribution-guidelines) + - [Git Commit Message Guidelines](#git-commit-message-guidelines) ## Building From Source @@ -73,7 +81,7 @@ When the build is complete, you will find binaries in `bin/`. ## Asking Support Questions -Feel free to post a question on our [GitHub Discussions](https://github.com/smallstep/certificates/discussions) page, or find us on [Gitter](https://gitter.im/smallstep/community). +Feel free to post a question on our [GitHub Discussions](https://github.com/smallstep/certificates/discussions) page, or find us on [Discord](https://bit.ly/step-discord). ## Reporting Issues diff --git a/docs/provisioners.md b/docs/provisioners.md index 7ee9af50..18010f88 100644 --- a/docs/provisioners.md +++ b/docs/provisioners.md @@ -1,7 +1,7 @@ # Provisioners > Note: The canonical documentation for `step-ca` provisioners now lives at -> https://smallstep.com/docs/step-ca/configuration#provisioners. Documentation +> https://smallstep.com/docs/step-ca/provisioners. Documentation > found on this page may be out of date. Provisioners are people or code that are registered with the CA and authorized diff --git a/docs/revocation.md b/docs/revocation.md index e994940d..4f3a7d5e 100644 --- a/docs/revocation.md +++ b/docs/revocation.md @@ -202,7 +202,8 @@ through an example. [Use TLS Everywhere](https://smallstep.com/blog/use-tls.html) and let us know what you think of our tools. Get in touch over [Twitter](twitter.com/smallsteplabs) or through our -[GitHub Discussions](https://github.com/smallstep/certificates/discussions) to chat with us in real time. +[GitHub Discussions](https://github.com/smallstep/certificates/discussions) to find answers to frequently asked questions. +[Discord](https://bit.ly/step-discord) to chat with us in real time. ## Further Reading diff --git a/go.mod b/go.mod index dcc6721e..f6228a9b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Masterminds/sprig/v3 v3.1.0 github.com/ThalesIgnite/crypto11 v1.2.4 github.com/aws/aws-sdk-go v1.30.29 + github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/go-kit/kit v0.10.0 // indirect github.com/go-piv/piv-go v1.7.0 @@ -22,7 +23,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/nosql v0.3.6 + github.com/smallstep/nosql v0.3.8 github.com/stretchr/testify v1.7.0 // indirect github.com/urfave/cli v1.22.4 go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 diff --git a/go.sum b/go.sum index f50932aa..60e9ab6b 100644 --- a/go.sum +++ b/go.sum @@ -136,9 +136,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= -github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658 h1:/WBjuutuivOA02gpDtrvrWKw01ugkyt3QnimB7enbtI= -github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658/go.mod h1:2uGEvGm+JSDLd5UAaKIFSbXDcYyeH0fWJP4N2HMMYMI= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -226,8 +227,6 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -328,6 +327,8 @@ github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -462,8 +463,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= 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/nosql v0.3.6 h1:cq6a3NwjFJxkVlWU1T4qGskcfEXr0fO1WqQrraDO1Po= -github.com/smallstep/nosql v0.3.6/go.mod h1:h1zC/Z54uNHc8euquLED4qJNCrMHd3nytA141ZZh4qQ= +github.com/smallstep/nosql v0.3.8 h1:1/EWUbbEdz9ai0g9Fd09VekVjtxp+5+gIHpV2PdwW3o= +github.com/smallstep/nosql v0.3.8/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index cfbf8235..f4c656d3 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -46,8 +46,8 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, }, apiv1.SHA512WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ - 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, - 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512, }, apiv1.SHA256WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ 0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, diff --git a/kms/uri/uri.go b/kms/uri/uri.go index 94009c47..44271e74 100644 --- a/kms/uri/uri.go +++ b/kms/uri/uri.go @@ -59,7 +59,9 @@ func Parse(rawuri string) (*URI, error) { if u.Scheme == "" { return nil, errors.Errorf("error parsing %s: scheme is missing", rawuri) } - v, err := url.ParseQuery(u.Opaque) + // Starting with Go 1.17 url.ParseQuery returns an error using semicolon as + // separator. + v, err := url.ParseQuery(strings.ReplaceAll(u.Opaque, ";", "&")) if err != nil { return nil, errors.Wrapf(err, "error parsing %s", rawuri) } diff --git a/kms/uri/uri_test.go b/kms/uri/uri_test.go index aa420db4..c2e0a9fe 100644 --- a/kms/uri/uri_test.go +++ b/kms/uri/uri_test.go @@ -274,3 +274,28 @@ func TestURI_Pin(t *testing.T) { }) } } + +func TestURI_String(t *testing.T) { + mustParse := func(s string) *URI { + u, err := Parse(s) + if err != nil { + t.Fatal(err) + } + return u + } + tests := []struct { + name string + uri *URI + want string + }{ + {"ok new", New("yubikey", url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}}), "yubikey:foo=bar;slot-id=9a"}, + {"ok parse", mustParse("yubikey:slot-id=9a;foo=bar?bar=zar"), "yubikey:slot-id=9a;foo=bar?bar=zar"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.uri.String(); got != tt.want { + t.Errorf("URI.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..80d3cdba --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,4 @@ +# Scripts folder + +Please note that `install-step-ra.sh` is referenced on the `files.smallstep.com` S3 website bucket as a redirect to `raw.githubusercontent.com`. If you move it, please update the S3 redirect. + diff --git a/scripts/install-step-ra.sh b/scripts/install-step-ra.sh new file mode 100644 index 00000000..1920b17d --- /dev/null +++ b/scripts/install-step-ra.sh @@ -0,0 +1,265 @@ +#!/bin/bash +set -e + +# TODO: +# - Parse params using argbash (argbash.io). Here's a template that I have tested but have not implemented yet: +# +# ARG_OPTIONAL_SINGLE([ca-url], , [the URL of the upstream (issuing) step-ca server]) +# ARG_OPTIONAL_SINGLE([fingerprint], , [the SHA256 fingerprint of the upstream peer step-ca server]) +# ARG_OPTIONAL_SINGLE([provisioner-name], , [the name of a JWK provisioner on the upstream CA that this RA will use]) +# ARG_OPTIONAL_SINGLE([provisioner-password-file], , [the name a file containing the upstream JWK provisioner password]) +# ARG_OPTIONAL_REPEATED([dns-name], , [DNS name of this RA that will appear on its TLS certificate; you may pass this flag multiple times]) +# ARG_OPTIONAL_SINGLE([listen-address], , [the address (and port #) this RA will listen on, eg. :443 or 127.0.0.1:4443]) +# ARG_HELP([This script will install and configure a Registration Authority that connects to an upstream CA running step-ca.]) +# ARGBASH_GO + +echo "This script will install and start a step-ca server running in Registration Authority (RA) mode." +echo "" +echo "You will need an upstream CA (URL and fingerprint)" +echo "Don't have a CA? Sign up for a hosted CA at smallstep.com — or run your own." +echo "" + +# Fail if this script is not run as root. +if ! [ $(id -u) = 0 ]; then + echo "This script must be run as root" + exit 1 +fi + +# Architecture detection +arch=$(uname -m) +case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; +esac + +if [ "$arch" = "armv5" ]; then + echo "This script doesn't work on armv5 machines" + exit 1 +fi + +if ! hash jq &> /dev/null; then + echo "This script requires the jq commmand; please install it." + exit 1 +fi + +if ! hash curl &> /dev/null; then + echo "This script requires the curl commmand; please install it." + exit 1 +fi + +if ! hash tar &> /dev/null; then + echo "This script requires the tar commmand; please install it." + exit 1 +fi + +while [ $# -gt 0 ]; do + case "$1" in + --ca-url) + CA_URL="$2" + shift + shift + ;; + --fingerprint) + CA_FINGERPRINT="$2" + shift + shift + ;; + --provisioner-name) + CA_PROVISIONER_NAME="$2" + shift + shift + ;; + --provisioner-password-file) + CA_PROVISIONER_JWK_PASSWORD_FILE="$2" + shift + shift + ;; + --dns-names) + RA_DNS_NAMES="$2" + shift + shift + ;; + --listen-address) + RA_ADDRESS="$2" + shift + shift + ;; + *) + shift + ;; + esac +done + +# Install step +if ! hash step &> /dev/null; then + echo "Installing 'step' in /usr/bin..." + STEP_VERSION=$(curl -s https://api.github.com/repos/smallstep/cli/releases/latest | jq -r '.tag_name') + + curl -sLO https://github.com/smallstep/cli/releases/download/$STEP_VERSION/step_linux_${STEP_VERSION:1}_$arch.tar.gz + tar xvzf step_linux_${STEP_VERSION:1}_$arch.tar.gz + install -m 0755 -t /usr/bin step_${STEP_VERSION:1}/bin/step + + rm step_linux_${STEP_VERSION:1}_$arch.tar.gz + rm -rf step_${STEP_VERSION:1} +fi + +# Prompt for required parameters +if [ -z "$CA_URL" ]; then + CA_URL="" + while [[ $CA_URL = "" ]]; do + read -p "Issuing CA URL: " CA_URL < /dev/tty + done +fi + +if [ -z "$CA_FINGERPRINT" ]; then + CA_FINGERPRINT="" + while [[ $CA_FINGERPRINT = "" ]]; do + read -p "Issuing CA Fingerprint: " CA_FINGERPRINT < /dev/tty + done +fi + +echo "Bootstrapping with the CA..." +export STEPPATH=$(mktemp -d) +export STEP_CONSOLE=true + +step ca bootstrap --ca-url $CA_URL --fingerprint $CA_FINGERPRINT + +if [ -z "$CA_PROVISIONER_NAME" ]; then + declare -a provisioners + readarray -t provisioners < <(step ca provisioner list | jq -r '.[] | select(.type == "JWK") | .name') + provisioners+=("Create provisioner") + printf '%s\n' "${provisioners[@]}" + + printf "%b" "\nSelect a JWK provisioner:\n" >&2 + select provisioner in "${provisioners[@]}"; do + if [ "$provisioner" == "Create provisioner" ]; then + echo "Creating a JWK provisioner on the upstream CA..." + echo "" + read -p "Label your provisioner (e.g. example-ra): " CA_PROVISIONER_NAME < /dev/tty + step beta ca provisioner add $CA_PROVISIONER_NAME --type JWK --create + break + elif [ -n "$provisioner" ]; then + echo "Using existing provisioner $provisioner." + CA_PROVISIONER_NAME=$provisioner + break + else + echo "Invalid selection!" + fi + done +fi + +if [ -z "$RA_DNS_NAMES" ]; then + RA_DNS_NAMES="" + while [[ $RA_DNS_NAMES = "" ]]; do + echo "What DNS names or IP addresses will your RA use?" + read -p "(e.g. acme.example.com[,1.1.1.1,etc.]): " RA_DNS_NAMES < /dev/tty + done +fi + +if [ -z "$RA_ADDRESS" ]; then + RA_ADDRESS="" + while [[ $RA_ADDRESS = "" ]] ; do + echo "What address should your RA listen on?" + read -p "(e.g. :443 or 10.2.1.201:4430): " RA_ADDRESS < /dev/tty + done +fi + +if [ -z "$CA_PROVISIONER_JWK_PASSWORD_FILE" ]; then + read -s -p "Enter the CA Provisioner Password: " CA_PROVISIONER_JWK_PASSWORD < /dev/tty + printf "%b" "\n" +fi + +echo "Installing 'step-ca' in /usr/bin..." +CA_VERSION=$(curl -s https://api.github.com/repos/smallstep/certificates/releases/latest | jq -r '.tag_name') + +curl -sLO https://github.com/smallstep/certificates/releases/download/$CA_VERSION/step-ca_linux_${CA_VERSION:1}_$arch.tar.gz +tar -xf step-ca_linux_${CA_VERSION:1}_$arch.tar.gz +install -m 0755 -t /usr/bin step-ca_${CA_VERSION:1}/bin/step-ca +setcap CAP_NET_BIND_SERVICE=+eip $(which step-ca) +rm step-ca_linux_${CA_VERSION:1}_$arch.tar.gz +rm -rf step-ca_${CA_VERSION:1} + +echo "Creating 'step' user..." +export STEPPATH=/etc/step-ca + +useradd --system --home $(step path) --shell /bin/false step + +echo "Creating RA configuration..." +mkdir -p $(step path)/db +mkdir -p $(step path)/config + +cat < $(step path)/config/ca.json +{ + "address": "$RA_ADDRESS", + "dnsNames": ["$RA_DNS_NAMES"], + "db": { + "type": "badgerV2", + "dataSource": "/etc/step-ca/db" + }, + "logger": {"format": "text"}, + "authority": { + "type": "stepcas", + "certificateAuthority": "$CA_URL", + "certificateAuthorityFingerprint": "$CA_FINGERPRINT", + "certificateIssuer": { + "type" : "jwk", + "provisioner": "$CA_PROVISIONER_NAME" + }, + "provisioners": [{ + "type": "ACME", + "name": "acme" + }] + }, + "tls": { + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + ], + "minVersion": 1.2, + "maxVersion": 1.3, + "renegotiation": false + } +} +EOF + +if ! [ -z "$CA_PROVISIONER_JWK_PASSWORD" ]; then + echo "Saving provisoiner password to $(step path)/password.txt..." + echo $CA_PROVISIONER_JWK_PASSWORD > $(step path)/password.txt +else + echo "Copying provisioner password file to $(step path)/password.txt..." + cp $CA_PROVISIONER_JWK_PASSWORD_FILE $(step path)/password.txt +fi +chmod 440 $(step path)/password.txt + +# Add a service to systemd for the RA. +echo "Creating systemd service step-ca.service..." +curl -sL https://raw.githubusercontent.com/smallstep/certificates/master/systemd/step-ca.service \ + -o /etc/systemd/system/step-ca.service + +echo "Creating RA mode override /etc/systemd/system/step-ca.service.d/local.conf..." +mkdir /etc/systemd/system/step-ca.service.d +cat < /etc/systemd/system/step-ca.service.d/local.conf +[Service] +; The empty ExecStart= clears the inherited ExecStart= value +ExecStart= +ExecStart=/usr/bin/step-ca config/ca.json --issuer-password-file password.txt +EOF + +echo "Starting step-ca.service..." +systemctl daemon-reload + +chown -R step:step $(step path) + +systemctl enable --now step-ca + +echo "Adding STEPPATH export to /root/.bash_profile..." +echo "export STEPPATH=$STEPPATH" >> /root/.bash_profile + +echo "Finished. Check the journal with journalctl -fu step-ca.service" + diff --git a/systemd/cert-renewer@.service b/systemd/cert-renewer@.service index f38951b5..5b56f5fc 100644 --- a/systemd/cert-renewer@.service +++ b/systemd/cert-renewer@.service @@ -12,14 +12,10 @@ Environment=STEPPATH=/etc/step-ca \ CERT_LOCATION=/etc/step/certs/%i.crt \ KEY_LOCATION=/etc/step/certs/%i.key -; ExecStartPre checks if the certificate is ready for renewal, +; ExecCondition checks if the certificate is ready for renewal, ; based on the exit status of the command. -; (In systemd 243 and above, you can use ExecCondition= here.) -ExecStartPre=/usr/bin/env bash -c \ - 'step certificate inspect $CERT_LOCATION --format json --roots "$STEPPATH/certs/root_ca.crt" | \ - jq -e "(((.validity.start | fromdate) + \ - ((.validity.end | fromdate) - (.validity.start | fromdate)) * 0.66) \ - - now) <= 0" > /dev/null' +; (In systemd 242 or below, you can use ExecStartPre= here.) +ExecCondition=/usr/bin/step certificate needs-renewal $CERT_LOCATION ; ExecStart renews the certificate, if ExecStartPre was successful. ExecStart=/usr/bin/step ca renew --force $CERT_LOCATION $KEY_LOCATION