diff --git a/.devcontainer/.env b/.devcontainer/.env new file mode 100644 index 000000000..15181a657 --- /dev/null +++ b/.devcontainer/.env @@ -0,0 +1,4 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres +POSTGRES_HOSTNAME=localhost diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..4feec8622 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.18, 1.17, 1-bullseye, 1.18-bullseye, 1.17-bullseye, 1-buster, 1.18-buster, 1.17-buster +ARG VARIANT=1-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment the next lines to use go get to install anything else you need +# USER vscode +# RUN go get -x +# USER root + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..04480f801 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/go-postgres +{ + "name": "Go & PostgreSQL", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go", + "go.goroot": "/usr/local/go" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "golang.Go" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5432], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..d299ccd67 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +volumes: + postgres-data: + + null +services: + app: + build: + context: . + dockerfile: Dockerfile + args: + # [Choice] Go version 1, 1.18, 1.17 + # Append -bullseye or -buster to pin to an OS version. + # Use -bullseye variants on local arm64/Apple Silicon. + VARIANT: 1.18-bullseye + # Options + NODE_VERSION: "lts/*" + env_file: + # Ensure that the variables in .env match the same variables in devcontainer.json + - .env + + # Security Opt and cap_add allow for C++ based debuggers to work. + # See `runArgs`: https://github.com/Microsoft/vscode-docs/blob/main/docs/remote/devcontainerjson-reference.md + # security_opt: + # - seccomp:unconfined + # cap_add: + # - SYS_PTRACE + + volumes: + - ..:/workspace:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + # Uncomment the next line to use a non-root user for all processes. + # user: vscode + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + db: + image: postgres:latest + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + # Ensure that the variables in .env match the same variables in devcontainer.json + - .env + # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 000000000..cd633663e --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,7 @@ +version: "1" +rules: + - base: master + upstream: smallstep:master + mergeMethod: merge + assignees: + - fritterhoff diff --git a/.github/workflows/code-scan-cron.yml b/.github/workflows/code-scan-cron.yml deleted file mode 100644 index 9a35b7fe6..000000000 --- a/.github/workflows/code-scan-cron.yml +++ /dev/null @@ -1,7 +0,0 @@ -on: - schedule: - - cron: '0 0 * * *' - -jobs: - code-scan: - uses: smallstep/workflows/.github/workflows/code-scan.yml@main diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml deleted file mode 100644 index 6efe37ff9..000000000 --- a/.github/workflows/dependabot-auto-merge.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Dependabot auto-merge -on: pull_request - -permissions: - contents: write - pull-requests: write - -jobs: - dependabot: - runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v2.1.0 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - name: Enable auto-merge for Dependabot PRs - run: gh pr merge --auto --merge "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..faa7eea90 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,91 @@ +name: Docker Images + +on: + push: + pull_request: + branches: + - 'main' + +jobs: + build: + runs-on: ubuntu-latest + env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: hm-edu/certificates + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=raw,value={{branch}}-{{sha}}-{{date 'X'}},enable=${{ github.event_name != 'pull_request' }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: docker/Dockerfile + build-agent: + runs-on: ubuntu-latest + env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: hm-edu/certificates-agent + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=raw,value={{branch}}-{{sha}}-{{date 'X'}},enable=${{ github.event_name != 'pull_request' }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: docker/Dockerfile.agent diff --git a/.gitignore b/.gitignore index 42e960498..543a0521f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ output vendor .idea .envrc +step-ca diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..c6db162df --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/step-ca", + } + ] +} \ No newline at end of file diff --git a/acme/api/account.go b/acme/api/account.go index 62d62f099..13eb3fd12 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -149,7 +149,7 @@ func NewAccount(w http.ResponseWriter, r *http.Request) { render.Error(w, err) return } - if err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil { + if err := db.UpdateExternalAccountKey(ctx, prov.GetID(), eak); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key")) return } diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 7d799c883..8febbf5d4 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -314,14 +314,14 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { "fail/nil-account": func(t *testing.T) test { return test{ db: &acme.MockDB{}, - ctx: context.WithValue(context.Background(), accContextKey, http.NoBody), + ctx: context.WithValue(context.Background(), AccContextKey, http.NoBody), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), } }, "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "foo"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{}, @@ -332,7 +332,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { }, "fail/db.GetOrdersByAccountID-error": func(t *testing.T) test { acc := &acme.Account{ID: accID} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -347,7 +347,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { acc := &acme.Account{ID: accID} ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx = acme.NewProvisionerContext(ctx, prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) return test{ db: &acme.MockDB{ MockGetOrdersByAccountID: func(ctx context.Context, id string) ([]string, error) { @@ -682,7 +682,7 @@ func TestHandler_NewAccount(t *testing.T) { } ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -863,7 +863,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), accContextKey, nil) + ctx := context.WithValue(context.Background(), AccContextKey, nil) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -872,7 +872,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } }, "fail/no-payload": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), accContextKey, &acc) + ctx := context.WithValue(context.Background(), AccContextKey, &acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -881,7 +881,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } }, "fail/nil-payload": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), accContextKey, &acc) + ctx := context.WithValue(context.Background(), AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ db: &acme.MockDB{}, @@ -891,7 +891,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } }, "fail/unmarshal-payload-error": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), accContextKey, &acc) + ctx := context.WithValue(context.Background(), AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{}) return test{ db: &acme.MockDB{}, @@ -906,7 +906,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } b, err := json.Marshal(uar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), accContextKey, &acc) + ctx := context.WithValue(context.Background(), AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{}, @@ -921,7 +921,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { } b, err := json.Marshal(uar) assert.FatalError(t, err) - ctx := context.WithValue(context.Background(), accContextKey, &acc) + ctx := context.WithValue(context.Background(), AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{ @@ -943,7 +943,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { b, err := json.Marshal(uar) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, &acc) + ctx = context.WithValue(ctx, AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{ @@ -962,7 +962,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { b, err := json.Marshal(uar) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, &acc) + ctx = context.WithValue(ctx, AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{}, @@ -977,7 +977,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { b, err := json.Marshal(uar) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, &acc) + ctx = context.WithValue(ctx, AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{ @@ -993,7 +993,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) { }, "ok/post-as-get": func(t *testing.T) test { ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, &acc) + ctx = context.WithValue(ctx, AccContextKey, &acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true}) return test{ db: &acme.MockDB{}, diff --git a/acme/api/eab.go b/acme/api/eab.go index 26854595b..8026ae829 100644 --- a/acme/api/eab.go +++ b/acme/api/eab.go @@ -51,27 +51,27 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) } db := acme.MustDatabaseFromContext(ctx) - externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) + externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.GetID(), keyID) if err != nil { var ae *acme.Error if errors.As(err, &ae) { return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") } - return nil, acme.WrapErrorISE(err, "error retrieving external account key") + return nil, acme.NewError(acme.ErrorEabDoesNotExistType, "error retrieving external account key") } if externalAccountKey == nil { return nil, acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key") } - if len(externalAccountKey.HmacKey) == 0 { - return nil, acme.NewError(acme.ErrorServerInternalType, "external account binding key with id '%s' does not have secret bytes", keyID) - } - if externalAccountKey.AlreadyBound() { return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt) } + if len(externalAccountKey.HmacKey) == 0 { + return nil, acme.NewError(acme.ErrorEabAlreadyUsedType, "external account binding key with id '%s' does not have secret bytes", keyID) + } + payload, err := eabJWS.Verify(externalAccountKey.HmacKey) if err != nil { return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature") diff --git a/acme/api/eab_test.go b/acme/api/eab_test.go index 14dbdad1f..ec1906dcd 100644 --- a/acme/api/eab_test.go +++ b/acme/api/eab_test.go @@ -312,7 +312,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewErrorISE("error retrieving external account key"), + err: acme.NewError(acme.ErrorEabDoesNotExistType, "error retrieving external account key"), } }, "fail/db.GetExternalAccountKey-not-found": func(t *testing.T) test { @@ -361,7 +361,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewErrorISE("error retrieving external account key"), + err: acme.NewError(acme.ErrorEabDoesNotExistType, "error retrieving external account key"), } }, "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { @@ -410,7 +410,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewErrorISE("error retrieving external account key"), + err: acme.NewError(acme.ErrorEabDoesNotExistType, "error retrieving external account key"), } }, "fail/db.GetExternalAccountKey-nil": func(t *testing.T) test { @@ -516,7 +516,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) { ExternalAccountBinding: eab, }, eak: nil, - err: acme.NewError(acme.ErrorServerInternalType, "external account binding key with id 'eakID' does not have secret bytes"), + err: acme.NewError(acme.ErrorEabAlreadyUsedType, "external account binding key with id 'eakID' does not have secret bytes"), } }, "fail/db.GetExternalAccountKey-wrong-provisioner": func(t *testing.T) test { diff --git a/acme/api/handler.go b/acme/api/handler.go index d2940f49d..c32fd88c7 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -5,16 +5,22 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "errors" "fmt" "net/http" "time" "github.com/go-chi/chi/v5" + "github.com/sirupsen/logrus" + + pb "github.com/hm-edu/portal-apis" + "github.com/smallstep/certificates/cas/sectigocas/eab" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" ) @@ -68,6 +74,7 @@ type HandlerOptions struct { // PrerequisitesChecker checks if all prerequisites for serving ACME are // met by the CA configuration. PrerequisitesChecker func(ctx context.Context) (bool, error) + Cfg *config.Config } var mustAuthority = func(ctx context.Context) acme.CertificateAuthority { @@ -102,7 +109,6 @@ func (h *handler) Route(r api.Router) { } // NewHandler returns a new ACME API handler. -// // Note: this method is deprecated in step-ca, other applications can still use // this to support ACME, but the recommendation is to use use // api.Route(api.Router) and acme.NewContext() instead. @@ -397,3 +403,23 @@ func GetCertificate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/pem-certificate-chain") w.Write(certBytes) } + +func checkPermission(ctx context.Context, identifiers []acme.Identifier, eak *acme.ExternalAccountKey) ([]string, error) { + if eak == nil { + logrus.Warn("No external account key given. Cannot check permissions") + return nil, nil + } + var domains []string + for _, x := range identifiers { + domains = append(domains, x.Value) + } + client, ok := eab.FromContext(ctx) + if !ok { + return nil, errors.New("no external account client available") + } + result, err := client.CheckEABPermissions(ctx, &pb.CheckEABPermissionRequest{Domains: domains, EabKey: eak.ID}) + if err != nil { + return nil, err + } + return result.Missing, nil +} diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index bd7bb50e3..44a78d698 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -17,6 +17,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/go-cmp/cmp" + pb "github.com/hm-edu/portal-apis" "github.com/pkg/errors" "go.step.sm/crypto/jose" @@ -25,8 +26,21 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" + "google.golang.org/grpc" ) +type MockClient struct { + Missing []string +} + +func (c *MockClient) CheckEABPermissions(ctx context.Context, in *pb.CheckEABPermissionRequest, opts ...grpc.CallOption) (*pb.CheckEABPermissionResponse, error) { + return &pb.CheckEABPermissionResponse{Missing: c.Missing}, nil +} + +func (c *MockClient) ResolveAccountId(ctx context.Context, in *pb.ResolveAccountIdRequest, opts ...grpc.CallOption) (*pb.ResolveAccountIdResponse, error) { //nolint + return nil, nil +} + type mockClient struct { get func(url string) (*http.Response, error) lookupTxt func(name string) ([]string, error) @@ -263,7 +277,7 @@ func TestHandler_GetAuthorization(t *testing.T) { }, "fail/nil-account": func(t *testing.T) test { ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, nil) + ctx = context.WithValue(ctx, AccContextKey, nil) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -273,7 +287,7 @@ func TestHandler_GetAuthorization(t *testing.T) { }, "fail/db.GetAuthorization-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -286,7 +300,7 @@ func TestHandler_GetAuthorization(t *testing.T) { }, "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -304,7 +318,7 @@ func TestHandler_GetAuthorization(t *testing.T) { }, "fail/db.UpdateAuthorization-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -329,7 +343,7 @@ func TestHandler_GetAuthorization(t *testing.T) { "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -426,7 +440,7 @@ func TestHandler_GetCertificate(t *testing.T) { } }, "fail/nil-account": func(t *testing.T) test { - ctx := context.WithValue(context.Background(), accContextKey, nil) + ctx := context.WithValue(context.Background(), AccContextKey, nil) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -436,7 +450,7 @@ func TestHandler_GetCertificate(t *testing.T) { }, "fail/db.GetCertificate-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -449,7 +463,7 @@ func TestHandler_GetCertificate(t *testing.T) { }, "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -465,7 +479,7 @@ func TestHandler_GetCertificate(t *testing.T) { }, "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -549,14 +563,14 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/nil-account": func(t *testing.T) test { return test{ db: &acme.MockDB{}, - ctx: context.WithValue(context.Background(), accContextKey, nil), + ctx: context.WithValue(context.Background(), AccContextKey, nil), statusCode: 400, err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), } }, "fail/no-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -567,7 +581,7 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/nil-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ db: &acme.MockDB{}, @@ -579,7 +593,7 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/db.GetChallenge-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -598,7 +612,7 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -617,7 +631,7 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/no-jwk": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -636,7 +650,7 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/nil-jwk": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) ctx = context.WithValue(ctx, jwkContextKey, nil) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -656,7 +670,7 @@ func TestHandler_GetChallenge(t *testing.T) { "fail/validate-challenge-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) _jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) @@ -669,9 +683,11 @@ func TestHandler_GetChallenge(t *testing.T) { assert.Equals(t, chID, "chID") assert.Equals(t, azID, "authzID") return &acme.Challenge{ - Status: acme.StatusPending, - Type: acme.HTTP01, - AccountID: "accID", + ID: chID, + AuthorizationID: azID, + Status: acme.StatusPending, + Type: acme.HTTP01, + AccountID: "accID", }, nil }, MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error { @@ -696,7 +712,7 @@ func TestHandler_GetChallenge(t *testing.T) { "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) _jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) diff --git a/acme/api/middleware.go b/acme/api/middleware.go index a7ed81c8c..f25e60487 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -258,7 +258,7 @@ func extractJWK(next nextHTTP) nextHTTP { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active")) return } - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) } next(w, r.WithContext(ctx)) } @@ -359,7 +359,7 @@ func lookupJWK(next nextHTTP) nextHTTP { return } } - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, jwkContextKey, acc.Key) next(w, r.WithContext(ctx)) return @@ -566,7 +566,7 @@ type ContextKey string const ( // accContextKey account key - accContextKey = ContextKey("acc") + AccContextKey = ContextKey("acc") // jwsContextKey jws key jwsContextKey = ContextKey("jws") // jwkContextKey jwk key @@ -578,7 +578,7 @@ const ( // accountFromContext searches the context for an ACME account. Returns the // account or an error. func accountFromContext(ctx context.Context) (*acme.Account, error) { - val, ok := ctx.Value(accContextKey).(*acme.Account) + val, ok := ctx.Value(AccContextKey).(*acme.Account) if !ok || val == nil { return nil, acme.NewError(acme.ErrorAccountDoesNotExistType, "account not in context") } diff --git a/acme/api/order.go b/acme/api/order.go index b207f87ce..31b27ae4f 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -137,7 +137,7 @@ func NewOrder(w http.ResponseWriter, r *http.Request) { var eak *acme.ExternalAccountKey if acmeProv.RequireEAB { if eak, err = db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil { - render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key")) + render.Error(w, acme.NewError(acme.ErrorEabAccountBindingDoesNotExistType, "error retrieving external account binding key")) return } } @@ -180,6 +180,17 @@ func NewOrder(w http.ResponseWriter, r *http.Request) { NotAfter: nor.NotAfter, } + if acmeProv.RequireEAB { + if missing, err := checkPermission(ctx, o.Identifiers, eak); len(missing) != 0 || err != nil { + if err != nil { + render.Error(w, acme.NewError(acme.ErrorServerInternalType, "Internal server error")) + return + } + render.Error(w, acme.NewError(acme.ErrorRejectedIdentifierType, "Missing registration for domain(s) %v", missing)) + return + } + } + for i, identifier := range o.Identifiers { az := &acme.Authorization{ AccountID: acc.ID, @@ -320,6 +331,15 @@ func GetOrder(w http.ResponseWriter, r *http.Request) { linker.LinkOrder(ctx, o) w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID)) + if o.Status == acme.StatusProcessing { + // Due to the bad behavior of the k8s cert-manager we must catch this client using the User-Agent and handle it in a special way. + // The cert-manager does not repect the retry after flags and will retry too fast. + if strings.Contains(r.UserAgent(), "cert-manager") { + render.Error(w, acme.NewErrorISE("Request is processing")) + return + } + w.Header().Set("Retry-After", "10") + } render.JSON(w, o) } @@ -372,14 +392,14 @@ func FinalizeOrder(w http.ResponseWriter, r *http.Request) { } ca := mustAuthority(ctx) - if err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil { + if _, err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil { render.Error(w, acme.WrapErrorISE(err, "error finalizing order")) return } linker.LinkOrder(ctx, o) - w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID)) + w.Header().Set("Retry-After", "20") render.JSON(w, o) } diff --git a/acme/api/order_test.go b/acme/api/order_test.go index 36de975a3..f78600bd3 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -15,6 +15,8 @@ import ( "testing" "time" + "github.com/smallstep/certificates/cas/sectigocas/eab" + "github.com/go-chi/chi/v5" "github.com/pkg/errors" @@ -328,7 +330,7 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/nil-account": func(t *testing.T) test { ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, nil) + ctx = context.WithValue(ctx, AccContextKey, nil) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -338,7 +340,7 @@ func TestHandler_GetOrder(t *testing.T) { }, "fail/no-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -349,7 +351,7 @@ func TestHandler_GetOrder(t *testing.T) { "fail/nil-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), nil) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -360,7 +362,7 @@ func TestHandler_GetOrder(t *testing.T) { "fail/db.GetOrder-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -374,7 +376,7 @@ func TestHandler_GetOrder(t *testing.T) { "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -390,7 +392,7 @@ func TestHandler_GetOrder(t *testing.T) { "fail/provisioner-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -406,7 +408,7 @@ func TestHandler_GetOrder(t *testing.T) { "fail/order-update-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -430,7 +432,7 @@ func TestHandler_GetOrder(t *testing.T) { "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ db: &acme.MockDB{ @@ -799,6 +801,7 @@ func TestHandler_NewOrder(t *testing.T) { ctx context.Context nor *NewOrderRequest statusCode int + missing []string vr func(t *testing.T, o *acme.Order) err *acme.Error } @@ -813,7 +816,7 @@ func TestHandler_NewOrder(t *testing.T) { }, "fail/nil-account": func(t *testing.T) test { ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, nil) + ctx = context.WithValue(ctx, AccContextKey, nil) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -823,7 +826,7 @@ func TestHandler_NewOrder(t *testing.T) { }, "fail/no-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -834,7 +837,7 @@ func TestHandler_NewOrder(t *testing.T) { "fail/nil-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -844,7 +847,7 @@ func TestHandler_NewOrder(t *testing.T) { }, "fail/no-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = acme.NewProvisionerContext(ctx, prov) return test{ db: &acme.MockDB{}, @@ -856,7 +859,7 @@ func TestHandler_NewOrder(t *testing.T) { "fail/nil-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ db: &acme.MockDB{}, @@ -868,7 +871,7 @@ func TestHandler_NewOrder(t *testing.T) { "fail/unmarshal-payload-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{}) return test{ db: &acme.MockDB{}, @@ -883,7 +886,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{}, @@ -902,7 +905,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), &acme.MockProvisioner{}) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, @@ -930,11 +933,11 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), acmeProv) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, - statusCode: 500, + statusCode: 400, ca: &mockCA{}, db: &acme.MockDB{ MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { @@ -943,7 +946,7 @@ func TestHandler_NewOrder(t *testing.T) { return nil, errors.New("force") }, }, - err: acme.NewErrorISE("error retrieving external account binding key: force"), + err: acme.NewError(acme.ErrorEabAccountBindingDoesNotExistType, "The used external account binding seems to be deleted"), } }, "fail/newACMEPolicyEngine-error": func(t *testing.T) test { @@ -958,7 +961,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), acmeProv) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, @@ -994,7 +997,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), acmeProv) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, @@ -1037,7 +1040,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), provWithPolicy) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, @@ -1080,7 +1083,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), provWithPolicy) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, @@ -1118,7 +1121,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ ctx: ctx, @@ -1152,7 +1155,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3 **acme.Challenge @@ -1217,6 +1220,36 @@ func TestHandler_NewOrder(t *testing.T) { err: acme.NewErrorISE("error creating order: force"), } }, + "fail/missingDomain": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + nor := &NewOrderRequest{ + Identifiers: []acme.Identifier{ + {Type: "dns", Value: "zap.internal"}, + {Type: "dns", Value: "*.zar.internal"}, + }, + } + b, err := json.Marshal(nor) + assert.FatalError(t, err) + p := newACMEProv(t) + p.RequireEAB = true + ctx := acme.NewProvisionerContext(context.Background(), p) + ctx = context.WithValue(ctx, AccContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) + return test{ + ctx: ctx, + statusCode: 400, + ca: &mockCA{}, + missing: []string{"zap.internal"}, + db: &acme.MockDB{MockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, prov.GetID(), provisionerID) + assert.Equals(t, "accID", accountID) + return &acme.ExternalAccountKey{ + ID: "test", + }, nil + }}, + err: acme.NewError(acme.ErrorRejectedIdentifierType, "account does not exist"), + } + }, "ok/multiple-authz": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} nor := &NewOrderRequest{ @@ -1228,7 +1261,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(nor) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3, ch4 **acme.Challenge @@ -1348,7 +1381,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(nor) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3 **acme.Challenge @@ -1445,7 +1478,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(nor) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3 **acme.Challenge @@ -1541,7 +1574,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(nor) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3 **acme.Challenge @@ -1638,7 +1671,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(nor) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3 **acme.Challenge @@ -1738,7 +1771,7 @@ func TestHandler_NewOrder(t *testing.T) { b, err := json.Marshal(nor) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), provWithPolicy) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) var ( ch1, ch2, ch3 **acme.Challenge @@ -1828,6 +1861,7 @@ func TestHandler_NewOrder(t *testing.T) { t.Run(name, func(t *testing.T) { mockMustAuthority(t, tc.ca) ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme")) + ctx = eab.NewContext(ctx, &MockClient{Missing: tc.missing}) req := httptest.NewRequest("GET", u, http.NoBody) req = req.WithContext(ctx) w := httptest.NewRecorder() @@ -1930,7 +1964,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/nil-account": func(t *testing.T) test { ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, nil) + ctx = context.WithValue(ctx, AccContextKey, nil) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -1940,7 +1974,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/no-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -1951,7 +1985,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/nil-provisioner": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) return test{ db: &acme.MockDB{}, ctx: ctx, @@ -1961,7 +1995,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { }, "fail/no-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} - ctx := context.WithValue(context.Background(), accContextKey, acc) + ctx := context.WithValue(context.Background(), AccContextKey, acc) ctx = acme.NewProvisionerContext(ctx, prov) return test{ db: &acme.MockDB{}, @@ -1973,7 +2007,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/nil-payload": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, nil) return test{ db: &acme.MockDB{}, @@ -1985,7 +2019,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/unmarshal-payload-error": func(t *testing.T) test { acc := &acme.Account{ID: "accID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{}) return test{ db: &acme.MockDB{}, @@ -2000,7 +2034,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { b, err := json.Marshal(fr) assert.FatalError(t, err) ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) return test{ db: &acme.MockDB{}, @@ -2013,7 +2047,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -2028,7 +2062,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/account-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -2045,7 +2079,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/provisioner-id-mismatch": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -2062,7 +2096,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "fail/order-finalize-error": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ @@ -2087,7 +2121,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { "ok": func(t *testing.T) test { acc := &acme.Account{ID: "accountID"} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) return test{ diff --git a/acme/api/revoke_test.go b/acme/api/revoke_test.go index 85b9a0326..4aa2571ad 100644 --- a/acme/api/revoke_test.go +++ b/acme/api/revoke_test.go @@ -688,7 +688,7 @@ func TestHandler_RevokeCert(t *testing.T) { ctx := acme.NewProvisionerContext(context.Background(), prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) - ctx = context.WithValue(ctx, accContextKey, nil) + ctx = context.WithValue(ctx, AccContextKey, nil) db := &acme.MockDB{ MockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) { assert.Equals(t, cert.SerialNumber.String(), serial) @@ -707,7 +707,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/account-not-valid": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusInvalid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -736,7 +736,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/account-not-authorized": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -815,7 +815,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/certificate-revoked-check-fails": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) @@ -848,7 +848,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/certificate-already-revoked": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -886,7 +886,7 @@ func TestHandler_RevokeCert(t *testing.T) { assert.FatalError(t, err) acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: invalidReasonCodePayloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -924,7 +924,7 @@ func TestHandler_RevokeCert(t *testing.T) { } acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), mockACMEProv) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -956,7 +956,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/ca.Revoke": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -988,7 +988,7 @@ func TestHandler_RevokeCert(t *testing.T) { "fail/ca.Revoke-already-revoked": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) db := &acme.MockDB{ @@ -1019,7 +1019,7 @@ func TestHandler_RevokeCert(t *testing.T) { "ok/using-account-key": func(t *testing.T) test { acc := &acme.Account{ID: "accountID", Status: acme.StatusValid} ctx := acme.NewProvisionerContext(context.Background(), prov) - ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, AccContextKey, acc) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes}) ctx = context.WithValue(ctx, jwsContextKey, jws) ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) diff --git a/acme/challenge.go b/acme/challenge.go index 9f3ca388d..39104d5a5 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -27,6 +27,7 @@ import ( "github.com/fxamacker/cbor/v2" "github.com/google/go-tpm/legacy/tpm2" + "github.com/sirupsen/logrus" "golang.org/x/exp/slices" "github.com/smallstep/go-attestation/attest" @@ -36,6 +37,7 @@ import ( "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" + "github.com/smallstep/certificates/acme/validation" "github.com/smallstep/certificates/authority/provisioner" ) @@ -117,11 +119,40 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb if InsecurePortHTTP01 != 0 { u.Host += ":" + strconv.Itoa(InsecurePortHTTP01) } - + go func() { + mqtt, ok := validation.FromContext(ctx) + if !ok { + return + } + req := validation.ValidationRequest{ + Authz: ch.AuthorizationID, + Challenge: ch.ID, + Target: u.String(), + } + data, err := json.Marshal(req) + if err != nil { + logrus.Warn(err) + return + } + if token := mqtt.GetClient().Publish(fmt.Sprintf("%s/jobs", mqtt.GetOrganization()), 1, false, data); token.Wait() && token.Error() != nil { + logrus.Warn(token.Error()) + } + logrus.Info("published validation request") + }() vc := MustClientFromContext(ctx) - resp, err := vc.Get(u.String()) - if err != nil { - return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err, + resp, errHttp := vc.Get(u.String()) + // get challenge again and check if it was already validated + chDb, errDb := db.GetChallenge(ctx, ch.ID, ch.AuthorizationID) + if errDb == nil { + logrus.WithField("challenge", chDb.ID).WithField("authz", chDb.AuthorizationID).Infof("challenge has status %s", chDb.Status) + if chDb.Status == StatusValid { + return nil + } + } else { + logrus.WithError(errDb).WithField("challenge", ch.ID).WithField("authz", ch.AuthorizationID).Warn("error getting challenge from db") + } + if errHttp != nil { + return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, errHttp, "error doing http GET for url %s", u)) } defer resp.Body.Close() @@ -150,9 +181,17 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb ch.Status = StatusValid ch.Error = nil ch.ValidatedAt = clock.Now().Format(time.RFC3339) - - if err = db.UpdateChallenge(ctx, ch); err != nil { - return WrapErrorISE(err, "error updating challenge") + for { + if err = db.UpdateChallenge(ctx, ch); err != nil { + if strings.Contains(err.Error(), "changed since last read") { + // If the challenge has changed since we read it, then we + // don't want to overwrite the error. + logrus.Warn("challenge changed since last read -> retry saving") + continue + } + return WrapErrorISE(err, "error updating challenge") + } + break } return nil } diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 5cede1c5c..97dfbb54a 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -450,6 +450,9 @@ func TestChallenge_Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -487,6 +490,9 @@ func TestChallenge_Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -529,6 +535,9 @@ func TestChallenge_Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -920,6 +929,9 @@ func TestHTTP01Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -955,6 +967,9 @@ func TestHTTP01Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -992,6 +1007,9 @@ func TestHTTP01Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -1030,6 +1048,9 @@ func TestHTTP01Validate(t *testing.T) { }, }, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -1065,6 +1086,11 @@ func TestHTTP01Validate(t *testing.T) { }, nil }, }, + db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, + }, err: NewErrorISE("error reading response body for url http://zap.internal/.well-known/acme-challenge/%s: force", ch.Token), } }, @@ -1089,6 +1115,11 @@ func TestHTTP01Validate(t *testing.T) { }, }, jwk: jwk, + db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, + }, err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"), } }, @@ -1116,6 +1147,9 @@ func TestHTTP01Validate(t *testing.T) { }, jwk: jwk, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -1159,6 +1193,9 @@ func TestHTTP01Validate(t *testing.T) { }, jwk: jwk, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -1203,6 +1240,9 @@ func TestHTTP01Validate(t *testing.T) { }, jwk: jwk, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) @@ -1246,6 +1286,9 @@ func TestHTTP01Validate(t *testing.T) { }, jwk: jwk, db: &MockDB{ + MockGetChallenge: func(ctx context.Context, id, authzID string) (*Challenge, error) { + return ch, nil + }, MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { assert.Equal(t, "chID", updch.ID) assert.Equal(t, "token", updch.Token) diff --git a/acme/db/nosql/eab.go b/acme/db/nosql/eab.go index e2a437ddf..1b58907e5 100644 --- a/acme/db/nosql/eab.go +++ b/acme/db/nosql/eab.go @@ -4,9 +4,12 @@ import ( "context" "crypto/rand" "encoding/json" + "fmt" "sync" "time" + "github.com/sirupsen/logrus" + "github.com/pkg/errors" "github.com/smallstep/certificates/acme" @@ -229,9 +232,52 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) } -func (db *DB) GetExternalAccountKeyByAccountID(context.Context, string, string) (*acme.ExternalAccountKey, error) { - //nolint:nilnil // legacy - return nil, nil +func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) { + externalAccountKeyMutex.RLock() + defer externalAccountKeyMutex.RUnlock() + + logrus.Debug(fmt.Sprintf("searching for eak keys bount to provisioner %v", provisionerID)) + // cursor and limit are ignored in open source, at least for now. + + var eakIDs []string + r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID)) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID) + } + // it may happen that no record is found; we'll continue with an empty slice + } else { + if err := json.Unmarshal(r, &eakIDs); err != nil { + return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID) + } + } + logrus.Debug(fmt.Sprintf("found %v eak keys (%v)", len(eakIDs), eakIDs)) + for _, eakID := range eakIDs { + if eakID == "" { + continue // shouldn't happen; just in case + } + eak, err := db.getDBExternalAccountKey(ctx, eakID) + if err != nil { + if !nosqlDB.IsErrNotFound(err) { + return nil, errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID) + } + } + logrus.Debug(fmt.Sprintf("loaded %v", eak)) + if eak.AccountID == accountID { + return &acme.ExternalAccountKey{ + ID: eak.ID, + HmacKey: eak.HmacKey, + ProvisionerID: eak.ProvisionerID, + Reference: eak.Reference, + AccountID: eak.AccountID, + CreatedAt: eak.CreatedAt, + BoundAt: eak.BoundAt, + }, nil + } + logrus.Debug(fmt.Sprintf("%s does not match %s", eak.AccountID, accountID)) + } + + return nil, errors.Errorf("ACME EAB Key for account id %s not found", accountID) } func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error { @@ -313,7 +359,14 @@ func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error { } if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil { - return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + if len(eakIDs) == 0 { + logrus.Warnf("error replacing empty eakID list for provisioner %s", provisionerID) + if err_internal := db.save(ctx, provisionerID, _new, []string{}, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err_internal != nil { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } + } else { + return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID) + } } return nil diff --git a/acme/errors.go b/acme/errors.go index 658ec6e0c..7797ccb1f 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -65,6 +65,12 @@ const ( ErrorUserActionRequiredType // ErrorNotImplementedType operation is not implemented ErrorNotImplementedType + // ErrorEabAlreadyUsedType the external account binding has already been used + ErrorEabAlreadyUsedType + // ErrorEabDoesNotExistType the external account binding does not exist + ErrorEabDoesNotExistType + // ErrorEabAccountBindingDoesNotExistType the external account binding does not exist + ErrorEabAccountBindingDoesNotExistType ) // String returns the string representation of the acme problem type, @@ -121,6 +127,12 @@ func (ap ProblemType) String() string { return "userActionRequired" case ErrorNotImplementedType: return "notImplemented" + case ErrorEabAlreadyUsedType: + return "eabAlreadyUsed" + case ErrorEabDoesNotExistType: + return "eabDoesNotExist" + case ErrorEabAccountBindingDoesNotExistType: + return "eabAccountBindingDoesNotExist" default: return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap)) } @@ -141,6 +153,21 @@ var ( status: 500, } errorMap = map[ProblemType]errorMetadata{ + ErrorEabAlreadyUsedType: { + typ: officialACMEPrefix + ErrorExternalAccountRequiredType.String(), + details: "The external account binding has already been used", + status: 400, + }, + ErrorEabDoesNotExistType: { + typ: officialACMEPrefix + ErrorExternalAccountRequiredType.String(), + details: "The used external account binding key id does not exist", + status: 400, + }, + ErrorEabAccountBindingDoesNotExistType: { + typ: officialACMEPrefix + ErrorExternalAccountRequiredType.String(), + details: "The used external account binding seems to be deleted", + status: 400, + }, ErrorAccountDoesNotExistType: { typ: officialACMEPrefix + ErrorAccountDoesNotExistType.String(), details: "Account does not exist", diff --git a/acme/mqtt/client.go b/acme/mqtt/client.go new file mode 100644 index 000000000..086493ba7 --- /dev/null +++ b/acme/mqtt/client.go @@ -0,0 +1,108 @@ +package mqtt + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/sirupsen/logrus" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/acme/validation" +) + +var clock acme.Clock + +func Connect(acmeDB acme.DB, host, user, password, organization string) (validation.MqttClient, error) { + opts := mqtt.NewClientOptions() + opts.SetOrderMatters(false) // Allow out of order messages (use this option unless in order delivery is essential) + opts.ConnectTimeout = time.Second // Minimal delays on connect + opts.WriteTimeout = time.Second // Minimal delays on writes + opts.KeepAlive = 10 // Keepalive every 10 seconds so we quickly detect network outages + opts.PingTimeout = time.Second // local broker so response should be quick + opts.ConnectRetry = true + opts.AutoReconnect = true + opts.ClientID = "acme" + opts.Username = user + opts.Password = password + opts.AddBroker(fmt.Sprintf("ssl://%s:8883", host)) + logrus.Infof("connecting to mqtt broker") + // Log events + opts.OnConnectionLost = func(cl mqtt.Client, err error) { + logrus.Println("mqtt connection lost") + } + opts.OnConnect = func(cl mqtt.Client) { + logrus.Println("mqtt connection established") + go func() { + cl.Subscribe(fmt.Sprintf("%s/data", organization), 1, func(client mqtt.Client, msg mqtt.Message) { + logrus.Printf("Received message on topic: %s\nMessage: %s\n", msg.Topic(), msg.Payload()) + ctx := context.Background() + data := msg.Payload() + var payload validation.ValidationResponse + err := json.Unmarshal(data, &payload) + if err != nil { + logrus.Errorf("error unmarshalling payload: %v", err) + return + } + + ch, err := acmeDB.GetChallenge(ctx, payload.Challenge, payload.Authz) + if err != nil { + logrus.Errorf("error getting challenge: %v", err) + return + } + + acc, err := acmeDB.GetAccount(ctx, ch.AccountID) + if err != nil { + logrus.Errorf("error getting account: %v", err) + return + } + expected, err := acme.KeyAuthorization(ch.Token, acc.Key) + + if payload.Content != expected || err != nil { + logrus.Errorf("invalid key authorization: %v", err) + return + } + u := &url.URL{Scheme: "http", Host: ch.Value, Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} + logrus.Infof("challenge %s validated using mqtt", u.String()) + + if ch.Status != acme.StatusPending && ch.Status != acme.StatusValid { + return + } + + ch.Status = acme.StatusValid + ch.Error = nil + ch.ValidatedAt = clock.Now().Format(time.RFC3339) + for { + if err = acmeDB.UpdateChallenge(ctx, ch); err != nil { + if strings.Contains(err.Error(), "changed since last read") { + // If the challenge has changed since we read it, then we + // don't want to overwrite the error. + logrus.Warn("challenge changed since last read -> retry saving") + continue + } + logrus.Errorf("error updating challenge: %v", err) + } + logrus.Infof("challenge %s updated to valid", u.String()) + break + } + + }) + }() + } + opts.OnReconnecting = func(mqtt.Client, *mqtt.ClientOptions) { + logrus.Println("mqtt attempting to reconnect") + } + + client := mqtt.NewClient(opts) + + if token := client.Connect(); token.WaitTimeout(30*time.Second) && token.Error() != nil { + logrus.Warn(token.Error()) + return nil, token.Error() + } + + connection := validation.BrokerConnection{Client: client, Organization: organization} + return connection, nil +} diff --git a/acme/order.go b/acme/order.go index 13c16b27c..da95adebe 100644 --- a/acme/order.go +++ b/acme/order.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/sirupsen/logrus" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x509util" @@ -67,6 +68,8 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { now := clock.Now() switch o.Status { + case StatusProcessing: + return nil case StatusInvalid: return nil case StatusValid: @@ -150,27 +153,29 @@ func (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, // Finalize signs a certificate if the necessary conditions for Order completion // have been met. -// -// TODO(mariano): Here or in the challenge validation we should perform some -// external validation using the identifier value and the attestation data. From -// a validation service we can get the list of SANs to set in the final -// certificate. -func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth CertificateAuthority, p Provisioner) error { +func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth CertificateAuthority, p Provisioner) (chan error, error) { if err := o.UpdateStatus(ctx, db); err != nil { - return err + return nil, err } switch o.Status { case StatusInvalid: - return NewError(ErrorOrderNotReadyType, "order %s has been abandoned", o.ID) + return nil, NewError(ErrorOrderNotReadyType, "order %s has been abandoned", o.ID) case StatusValid: - return nil + return nil, nil case StatusPending: - return NewError(ErrorOrderNotReadyType, "order %s is not ready", o.ID) + return nil, NewError(ErrorOrderNotReadyType, "order %s is not ready", o.ID) case StatusReady: break + case StatusProcessing: + return nil, NewErrorISE("order %s is already processing", o.ID) default: - return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) + return nil, NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) + } + + o.Status = StatusProcessing + if err := db.UpdateOrder(ctx, o); err != nil { + return nil, WrapErrorISE(err, "error updating order %s", o.ID) } // Get key fingerprint if any. And then compare it with the CSR fingerprint. @@ -179,15 +184,15 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques // and the attestation certificate are the same. fingerprint, err := o.getAuthorizationFingerprint(ctx, db) if err != nil { - return err + return nil, err } if fingerprint != "" { fp, err := keyutil.Fingerprint(csr.PublicKey) if err != nil { - return WrapErrorISE(err, "error calculating key fingerprint") + return nil, WrapErrorISE(err, "error calculating key fingerprint") } if subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 { - return NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID) + return nil, NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID) } } @@ -212,7 +217,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques // could result in unauthorized access if a relying system relies on the Common // Name in its authorization logic. if csr.Subject.CommonName != "" && csr.Subject.CommonName != permanentIdentifier { - return NewError(ErrorBadCSRType, "CSR Subject Common Name does not match identifiers exactly: "+ + return nil, NewError(ErrorBadCSRType, "CSR Subject Common Name does not match identifiers exactly: "+ "CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, permanentIdentifier) } break @@ -233,7 +238,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques defaultTemplate = x509util.DefaultLeafTemplate sans, err := o.sans(csr) if err != nil { - return err + return nil, err } data.SetSubjectAlternativeNames(sans...) } @@ -242,7 +247,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOps, err := p.AuthorizeSign(ctx, "") if err != nil { - return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner") + return nil, WrapErrorISE(err, "error retrieving authorization options from ACME provisioner") } // Unlike most of the provisioners, ACME's AuthorizeSign method doesn't // define the templates, and the template data used in WebHooks is not @@ -255,38 +260,53 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate) if err != nil { - return WrapErrorISE(err, "error creating template options from ACME provisioner") + return nil, WrapErrorISE(err, "error creating template options from ACME provisioner") } // Build extra signing options. signOps = append(signOps, templateOptions) signOps = append(signOps, extraOptions...) + ch := make(chan error) + go func() { + // Sign a new certificate. + certChain, err := auth.SignWithContext(ctx, csr, provisioner.SignOptions{ + NotBefore: provisioner.NewTimeDuration(o.NotBefore), + NotAfter: provisioner.NewTimeDuration(o.NotAfter), + }, signOps...) + if err != nil { + logrus.WithError(err).Error("error signing certificate") + o.Status = StatusInvalid + ch <- WrapErrorISE(err, "error signing certificate for order %s", o.ID) + if err = db.UpdateOrder(ctx, o); err != nil { + logrus.WithError(err).Error("error updating order") + } + return + } - // Sign a new certificate. - certChain, err := auth.SignWithContext(ctx, csr, provisioner.SignOptions{ - NotBefore: provisioner.NewTimeDuration(o.NotBefore), - NotAfter: provisioner.NewTimeDuration(o.NotAfter), - }, signOps...) - if err != nil { - return WrapErrorISE(err, "error signing certificate for order %s", o.ID) - } - - cert := &Certificate{ - AccountID: o.AccountID, - OrderID: o.ID, - Leaf: certChain[0], - Intermediates: certChain[1:], - } - if err := db.CreateCertificate(ctx, cert); err != nil { - return WrapErrorISE(err, "error creating certificate for order %s", o.ID) - } + cert := &Certificate{ + AccountID: o.AccountID, + OrderID: o.ID, + Leaf: certChain[0], + Intermediates: certChain[1:], + } + if err := db.CreateCertificate(ctx, cert); err != nil { + logrus.WithError(err).Error("error creating certificate") + o.Status = StatusInvalid + ch <- WrapErrorISE(err, "error creating certificate for order %s", o.ID) + if err = db.UpdateOrder(ctx, o); err != nil { + logrus.WithError(err).Error("error updating order") + } + return + } - o.CertificateID = cert.ID - o.Status = StatusValid - if err = db.UpdateOrder(ctx, o); err != nil { - return WrapErrorISE(err, "error updating order %s", o.ID) - } - return nil + o.CertificateID = cert.ID + o.Status = StatusValid + if err = db.UpdateOrder(ctx, o); err != nil { + logrus.WithError(err).Error("error updating order") + ch <- WrapErrorISE(err, "error updating order %s", o.ID) + } + }() + return ch, nil } func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativeName, error) { diff --git a/acme/order_test.go b/acme/order_test.go index 07372af07..6d08d856b 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -497,7 +497,16 @@ func TestOrder_Finalize(t *testing.T) { MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { return &Authorization{ID: id, Status: StatusValid}, nil }, - }, + MockUpdateOrder: func(ctx context.Context, updo *Order) error { + assert.Equals(t, updo.CertificateID, "") + assert.Equals(t, updo.Status, StatusProcessing) + assert.Equals(t, updo.ID, o.ID) + assert.Equals(t, updo.AccountID, o.AccountID) + assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) + assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs) + assert.Equals(t, updo.Identifiers, o.Identifiers) + return nil + }}, err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"), } }, @@ -541,6 +550,16 @@ func TestOrder_Finalize(t *testing.T) { MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { return &Authorization{ID: id, Status: StatusValid}, nil }, + MockUpdateOrder: func(ctx context.Context, updo *Order) error { + assert.Equals(t, updo.CertificateID, "") + assert.Equals(t, updo.Status, StatusProcessing) + assert.Equals(t, updo.ID, o.ID) + assert.Equals(t, updo.AccountID, o.AccountID) + assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) + assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs) + assert.Equals(t, updo.Identifiers, o.Identifiers) + return nil + }, }, err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"), } @@ -587,7 +606,15 @@ func TestOrder_Finalize(t *testing.T) { MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) { return &Authorization{ID: id, Status: StatusValid}, nil }, - }, + MockUpdateOrder: func(ctx context.Context, updo *Order) error { + assert.Equals(t, updo.CertificateID, "") + assert.Equals(t, updo.ID, o.ID) + assert.Equals(t, updo.AccountID, o.AccountID) + assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) + assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs) + assert.Equals(t, updo.Identifiers, o.Identifiers) + return nil + }}, err: NewErrorISE("error signing certificate for order oID: force"), } }, @@ -644,6 +671,14 @@ func TestOrder_Finalize(t *testing.T) { assert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz}) return errors.New("force") }, + MockUpdateOrder: func(ctx context.Context, updo *Order) error { + assert.Equals(t, updo.ID, o.ID) + assert.Equals(t, updo.AccountID, o.AccountID) + assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) + assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs) + assert.Equals(t, updo.Identifiers, o.Identifiers) + return nil + }, }, err: NewErrorISE("error creating certificate for order oID: force"), } @@ -703,8 +738,8 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) + assert.Equals(t, updo.CertificateID, "") + assert.Equals(t, updo.Status, StatusProcessing) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -792,8 +827,8 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) + // assert.Equals(t, updo.CertificateID, "certID") + // assert.Equals(t, updo.Status, StatusValid) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -896,8 +931,8 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) + // assert.Equals(t, updo.CertificateID, "certID") + // assert.Equals(t, updo.Status, StatusValid) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -995,8 +1030,8 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) + // assert.Equals(t, updo.CertificateID, "certID") + // assert.Equals(t, updo.Status, StatusValid) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -1062,8 +1097,6 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -1126,8 +1159,6 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -1193,8 +1224,6 @@ func TestOrder_Finalize(t *testing.T) { return nil }, MockUpdateOrder: func(ctx context.Context, updo *Order) error { - assert.Equals(t, updo.CertificateID, "certID") - assert.Equals(t, updo.Status, StatusValid) assert.Equals(t, updo.ID, o.ID) assert.Equals(t, updo.AccountID, o.AccountID) assert.Equals(t, updo.ExpiresAt, o.ExpiresAt) @@ -1209,7 +1238,9 @@ func TestOrder_Finalize(t *testing.T) { for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) - if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil { + ch, err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov) + + if err != nil { if assert.NotNil(t, tc.err) { var k *Error if errors.As(err, &k) { @@ -1223,7 +1254,23 @@ func TestOrder_Finalize(t *testing.T) { } } } else { - assert.Nil(t, tc.err) + select { + case e := <-ch: + if assert.NotNil(t, tc.err) { + switch k := e.(type) { + case *Error: + assert.Equals(t, k.Type, tc.err.Type) + assert.Equals(t, k.Detail, tc.err.Detail) + assert.Equals(t, k.Status, tc.err.Status) + assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) + assert.Equals(t, k.Detail, tc.err.Detail) + default: + assert.FatalError(t, errors.New("unexpected error type")) + } + } + case <-time.After(1 * time.Second): + assert.Nil(t, tc.err) + } } }) } diff --git a/acme/status.go b/acme/status.go index d9aae82dc..c4fd8e0b4 100644 --- a/acme/status.go +++ b/acme/status.go @@ -16,5 +16,6 @@ var ( StatusReady = Status("ready") //statusExpired = "expired" //statusActive = "active" + StatusProcessing = Status("processing") //statusProcessing = "processing" ) diff --git a/acme/validation/client.go b/acme/validation/client.go new file mode 100644 index 000000000..637d356a7 --- /dev/null +++ b/acme/validation/client.go @@ -0,0 +1,60 @@ +package validation + +import ( + "context" + + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +type ValidationResponse struct { + Authz string `json:"authz"` + Challenge string `json:"challenge"` + Content string `json:"content"` +} + +type ValidationRequest struct { + Authz string `json:"authz"` + Challenge string `json:"challenge"` + Target string `json:"target"` +} + +type validationKey struct{} + +type MqttClient interface { + GetClient() mqtt.Client + GetOrganization() string +} + +type BrokerConnection struct { + Client mqtt.Client + Organization string +} + +func (b BrokerConnection) GetClient() mqtt.Client { + return b.Client +} + +func (b BrokerConnection) GetOrganization() string { + return b.Organization +} + +// NewContext adds the given validation client to the context. +func NewContext(ctx context.Context, a MqttClient) context.Context { + return context.WithValue(ctx, validationKey{}, a) +} + +// FromContext returns the validation client from the given context. +func FromContext(ctx context.Context) (a MqttClient, ok bool) { + a, ok = ctx.Value(validationKey{}).(MqttClient) + return +} + +// MustFromContext returns the validation client from the given context. It will +// panic if no validation client is not in the context. +func MustFromContext(ctx context.Context) MqttClient { + if a, ok := FromContext(ctx); !ok { + panic("validation client is not in the context") + } else { + return a + } +} diff --git a/api/api.go b/api/api.go index 6916983b9..9ae1da2ab 100644 --- a/api/api.go +++ b/api/api.go @@ -43,9 +43,9 @@ type Authority interface { GetTLSOptions() *config.TLSOptions Root(shasum string) (*x509.Certificate, error) SignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) - Renew(peer *x509.Certificate) ([]*x509.Certificate, error) + Renew(ctx context.Context, peer *x509.Certificate) ([]*x509.Certificate, error) RenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) - Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) + Rekey(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error) LoadProvisionerByName(string) (provisioner.Interface, error) GetProvisioners(cursor string, limit int) (provisioner.List, string, error) @@ -55,6 +55,7 @@ type Authority interface { GetFederation() ([]*x509.Certificate, error) Version() authority.Version GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error) + Health() error } // mustAuthority will be replaced on unit tests. @@ -360,8 +361,14 @@ func Version(w http.ResponseWriter, r *http.Request) { } // Health is an HTTP handler that returns the status of the server. -func Health(w http.ResponseWriter, _ *http.Request) { - render.JSON(w, HealthResponse{Status: "ok"}) +func Health(w http.ResponseWriter, r *http.Request) { + a := mustAuthority(r.Context()) + err := a.Health() + if err == nil { + render.JSON(w, HealthResponse{Status: "ok"}) + } else { + render.JSONStatus(w, HealthResponse{Status: "error"}, http.StatusServiceUnavailable) + } } // Root is an HTTP handler that using the SHA256 from the URL, returns the root diff --git a/api/api_test.go b/api/api_test.go index 8090c6d43..6bf90ca5d 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -222,6 +222,10 @@ func (m *mockAuthority) GetCertificateRevocationList() (*authority.CertificateRe return m.ret1.(*authority.CertificateRevocationListInfo), m.err } +func (m *mockAuthority) Health() error { + return nil +} + // TODO: remove once Authorize is deprecated. func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { if m.authorize != nil { @@ -258,7 +262,7 @@ func (m *mockAuthority) SignWithContext(ctx context.Context, cr *x509.Certificat return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err } -func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { +func (m *mockAuthority) Renew(_ context.Context, cert *x509.Certificate) ([]*x509.Certificate, error) { if m.renew != nil { return m.renew(cert) } @@ -272,7 +276,7 @@ func (m *mockAuthority) RenewContext(ctx context.Context, oldcert *x509.Certific return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err } -func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { +func (m *mockAuthority) Rekey(_ context.Context, oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { if m.rekey != nil { return m.rekey(oldcert, pk) } @@ -816,6 +820,7 @@ func Test_caHandler_Route(t *testing.T) { func Test_Health(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com/health", http.NoBody) w := httptest.NewRecorder() + mockMustAuthority(t, &mockAuthority{}) Health(w, req) res := w.Result() diff --git a/api/rekey.go b/api/rekey.go index cda843a3d..9384a02e6 100644 --- a/api/rekey.go +++ b/api/rekey.go @@ -1,6 +1,7 @@ package api import ( + "context" "net/http" "github.com/smallstep/certificates/api/read" @@ -45,7 +46,7 @@ func Rekey(w http.ResponseWriter, r *http.Request) { } a := mustAuthority(r.Context()) - certChain, err := a.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey) + certChain, err := a.Rekey(context.Background(), r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey) if err != nil { render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Rekey")) return diff --git a/api/renew.go b/api/renew.go index 1b9ed95fa..fae219043 100644 --- a/api/renew.go +++ b/api/renew.go @@ -1,6 +1,7 @@ package api import ( + "context" "crypto/x509" "net/http" "strings" @@ -33,7 +34,7 @@ func Renew(w http.ResponseWriter, r *http.Request) { } a := mustAuthority(ctx) - certChain, err := a.RenewContext(ctx, cert, nil) + certChain, err := a.Renew(context.Background(), cert) if err != nil { render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew")) return diff --git a/api/sshRenew.go b/api/sshRenew.go index cd6d9bde6..b7ad63adc 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -109,7 +109,7 @@ func renewIdentityCertificate(r *http.Request, notBefore, notAfter time.Time) ([ cert.NotAfter = notAfter } - certChain, err := mustAuthority(r.Context()).Renew(cert) + certChain, err := mustAuthority(r.Context()).Renew(r.Context(), cert) if err != nil { return nil, err } diff --git a/authority/authority.go b/authority/authority.go index d3b932883..562c80162 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -197,6 +197,11 @@ func NewEmbedded(opts ...Option) (*Authority, error) { return a, nil } +// Health checks if the authority is stil alive. +func (a *Authority) Health() error { + return a.db.Ping() +} + type authorityKey struct{} // NewContext adds the given authority to the context. diff --git a/authority/authority_test.go b/authority/authority_test.go index a39dca8ef..39cf815aa 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -432,9 +432,9 @@ func TestNewEmbedded_GetTLSCertificate(t *testing.T) { a, err := NewEmbedded(WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))) assert.FatalError(t, err) - + name, _ := os.MkdirTemp("", "") // GetTLSCertificate - cert, err := a.GetTLSCertificate() + cert, err := a.GetTLSCertificate(name, false) assert.FatalError(t, err) assert.Equals(t, []string{"localhost"}, cert.Leaf.DNSNames) assert.True(t, cert.Leaf.IPAddresses[0].Equal(net.ParseIP("127.0.0.1"))) diff --git a/authority/config/config.go b/authority/config/config.go index ea7ce35da..5018497ea 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -63,6 +63,13 @@ var ( } ) +type MqttConfig struct { + Host string `json:"host"` + Username string `json:"username"` + Password string `json:"password"` + Organization string `json:"organization"` +} + // Config represents the CA configuration and it's mapped to a JSON object. type Config struct { Root multiString `json:"root"` @@ -70,6 +77,7 @@ type Config struct { IntermediateCert string `json:"crt"` IntermediateKey string `json:"key"` Address string `json:"address"` + PublicAddress string `json:"publicAddress"` InsecureAddress string `json:"insecureAddress"` DNSNames []string `json:"dnsNames"` KMS *kms.Options `json:"kms,omitempty"` @@ -85,6 +93,9 @@ type Config struct { CRL *CRLConfig `json:"crl,omitempty"` MetricsAddress string `json:"metricsAddress,omitempty"` SkipValidation bool `json:"-"` + Storage string `json:"storage,omitempty"` + ManagementHost string `json:"managementHost"` + ValidationBroker *MqttConfig `json:"validationBroker,omitempty"` // Keeps record of the filename the Config is read from loadedFromFilepath string diff --git a/authority/tls.go b/authority/tls.go index ebc9d0d81..1f6e848fa 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -14,6 +14,7 @@ import ( "math/big" "net" "net/http" + "os" "strings" "time" @@ -284,7 +285,7 @@ func (a *Authority) signX509(ctx context.Context, csr *x509.CertificateRequest, // Sign certificate lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) - resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ + resp, err := a.x509CAService.CreateCertificate(ctx, &casapi.CreateCertificateRequest{ Template: leaf, CSR: csr, Lifetime: lifetime, @@ -325,10 +326,10 @@ func (a *Authority) AreSANsAllowed(_ context.Context, sans []string) error { return a.policyEngine.AreSANsAllowed(sans) } -// Renew creates a new Certificate identical to the old certificate, except with -// a validity window that begins 'now'. -func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { - return a.RenewContext(context.Background(), oldCert, nil) +// Renew creates a new Certificate identical to the old certificate, except +// with a validity window that begins 'now'. +func (a *Authority) Renew(ctx context.Context, oldCert *x509.Certificate) ([]*x509.Certificate, error) { + return a.RenewContext(ctx, oldCert, nil) } // Rekey is used for rekeying and renewing based on the public key. If the @@ -341,8 +342,8 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error // have changed), 'SubjectKeyId' (different in case of rekey), and // 'NotBefore/NotAfter' (the validity duration of the new certificate should be // equal to the old one, but starting 'now'). -func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { - return a.RenewContext(context.Background(), oldCert, pk) +func (a *Authority) Rekey(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { + return a.RenewContext(ctx, oldCert, pk) } // RenewContext creates a new certificate identical to the old one, but it can @@ -460,7 +461,7 @@ func (a *Authority) renewContext(ctx context.Context, oldCert *x509.Certificate, // mode, this can be used to renew a certificate. token, _ := TokenFromContext(ctx) - resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{ + resp, err := a.x509CAService.RenewCertificate(ctx, &casapi.RenewCertificateRequest{ Template: newCert, Lifetime: lifetime, Backdate: backdate, @@ -659,7 +660,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // CAS operation, note that SoftCAS (default) is a noop. // The revoke happens when this is stored in the db. - _, err := a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{ + _, err := a.x509CAService.RevokeCertificate(ctx, &casapi.RevokeCertificateRequest{ Certificate: revokedCert, SerialNumber: rci.Serial, Reason: rci.Reason, @@ -857,16 +858,49 @@ func (a *Authority) GenerateCertificateRevocationList() error { } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. -func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { +func (a *Authority) GetTLSCertificate(storage string, renew bool) (*tls.Certificate, error) { fatal := func(err error) (*tls.Certificate, error) { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate") } + var priv crypto.PrivateKey + data, err := os.ReadFile(fmt.Sprintf("%s/%s", storage, "ca.key")) + switch { + case err != nil && os.IsNotExist(err): + // Generate default key. + priv, err = keyutil.GenerateDefaultKey() + if err != nil { + return fatal(err) + } + pemutil.Serialize(priv, pemutil.ToFile(fmt.Sprintf("%s/%s", storage, "ca.key"), 0600)) + case err != nil: + return fatal(err) + default: + priv, err = pemutil.ParseKey(data) + if err != nil { + return fatal(err) + } + } - // Generate default key. - priv, err := keyutil.GenerateDefaultKey() + keyPEM, err := pemutil.Serialize(priv) if err != nil { return fatal(err) } + data, err = os.ReadFile(fmt.Sprintf("%s/%s", storage, "ca.crt")) + + if !renew && err == nil { + cert, err := pemutil.ParseCertificateBundle(data) + if err != nil { + return fatal(err) + } else if cert[0].NotAfter.After(time.Now().Add(7 * 24 * time.Hour)) { + tlsCrt, err := tls.X509KeyPair(data, pem.EncodeToMemory(keyPEM)) + if err != nil { + return fatal(err) + } + tlsCrt.Leaf = cert[0] + return &tlsCrt, nil + } + } + signer, ok := priv.(crypto.Signer) if !ok { return fatal(errors.New("private key is not a crypto.Signer")) @@ -922,7 +956,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { lifetime = 24 * time.Hour } - resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ + resp, err := a.x509CAService.CreateCertificate(context.Background(), &casapi.CreateCertificateRequest{ Template: certTpl, CSR: cr, Lifetime: lifetime, @@ -944,10 +978,6 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { Bytes: crt.Raw, })...) } - keyPEM, err := pemutil.Serialize(priv) - if err != nil { - return fatal(err) - } tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM)) if err != nil { @@ -955,7 +985,9 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { } // Set leaf certificate tlsCrt.Leaf = resp.Certificate + os.WriteFile(fmt.Sprintf("%s/%s", storage, "ca.crt"), pemBlocks, 0600) return &tlsCrt, nil + } // RFC 5280, 5.2.5 diff --git a/authority/tls_test.go b/authority/tls_test.go index b481ca68c..57ff78e04 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -1062,9 +1062,9 @@ func TestAuthority_Renew(t *testing.T) { var certChain []*x509.Certificate if tc.auth != nil { - certChain, err = tc.auth.Renew(tc.cert) + certChain, err = tc.auth.Renew(context.Background(), tc.cert) } else { - certChain, err = a.Renew(tc.cert) + certChain, err = a.Renew(context.Background(), tc.cert) } if err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { @@ -1267,9 +1267,9 @@ func TestAuthority_Rekey(t *testing.T) { var certChain []*x509.Certificate if tc.auth != nil { - certChain, err = tc.auth.Rekey(tc.cert, tc.pk) + certChain, err = tc.auth.Rekey(context.Background(), tc.cert, tc.pk) } else { - certChain, err = a.Rekey(tc.cert, tc.pk) + certChain, err = a.Rekey(context.Background(), tc.cert, tc.pk) } if err != nil { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { @@ -1802,7 +1802,7 @@ func TestAuthority_constraints(t *testing.T) { t.Errorf("Authority.SignWithContext() error = %v, wantErr %v", err, tt.wantErr) } - _, err = auth.Renew(cert) + _, err = auth.Renew(context.Background(), cert) if (err != nil) != tt.wantErr { t.Errorf("Authority.Renew() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index 62c422d43..3fde61995 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -54,7 +54,7 @@ func startCABootstrapServer() *httptest.Server { if err != nil { panic(err) } - baseContext := buildContext(ca.auth, nil, nil, nil) + baseContext := buildContext(ca.auth, nil, nil, nil, nil, nil) srv.Config.Handler = ca.srv.Handler srv.Config.BaseContext = func(net.Listener) context.Context { return baseContext diff --git a/ca/ca.go b/ca/ca.go index 0b426ded2..cd5d85adf 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -6,10 +6,12 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io" "log" "net" "net/http" "net/url" + "os" "reflect" "strings" "sync" @@ -17,10 +19,23 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + + "github.com/sirupsen/logrus" + "github.com/pkg/errors" "github.com/smallstep/certificates/acme" acmeAPI "github.com/smallstep/certificates/acme/api" acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql" + acmeMqtt "github.com/smallstep/certificates/acme/mqtt" + "github.com/smallstep/certificates/acme/validation" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/admin" @@ -38,6 +53,10 @@ import ( "github.com/smallstep/nosql" "go.step.sm/cli-utils/step" "go.step.sm/crypto/x509util" + + pb "github.com/hm-edu/portal-apis" + + "github.com/smallstep/certificates/cas/sectigocas/eab" ) type options struct { @@ -144,11 +163,13 @@ type CA struct { auth *authority.Authority config *config.Config srv *server.Server + public *server.Server insecureSrv *server.Server metricsSrv *server.Server opts *options renewer *TLSRenewer compactStop chan struct{} + tp *sdktrace.TracerProvider } // New creates and initializes the CA with the given configuration and options. @@ -164,6 +185,31 @@ func New(cfg *config.Config, opts ...Option) (*CA, error) { // Init initializes the CA with the given configuration. func (ca *CA) Init(cfg *config.Config) (*CA, error) { + var exporter sdktrace.SpanExporter + var err error + if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") == "" { + exporter, err = stdout.New(stdout.WithWriter(io.Discard)) + } else { + opts := []otlptracegrpc.Option{otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"))} + client := otlptracegrpc.NewClient(opts...) + exporter, err = otlptrace.New(context.Background(), client) + } + if err != nil { + return nil, err + } + ca.tp = sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithBatcher(exporter), + sdktrace.WithResource( + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("certificates"), + )), + ) + otel.SetTracerProvider(ca.tp) + + otel.SetTextMapPropagator(b3.New()) + // Set password, it's ok to set nil password, the ca will prompt for them if // they are required. opts := []authority.Option{ @@ -202,28 +248,39 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { return nil, err } ca.auth = auth - var tlsConfig *tls.Config - var clientTLSConfig *tls.Config - if ca.opts.tlsConfig != nil { - // try using the tls Configuration supplied by the caller - log.Print("Using tls configuration supplied by the application") - tlsConfig = ca.opts.tlsConfig - clientTLSConfig = ca.opts.tlsConfig - } else { - // default to using the step-ca x509 Signer Interface - log.Print("Building new tls configuration using step-ca x509 Signer Interface") - tlsConfig, clientTLSConfig, err = ca.getTLSConfig(auth) - if err != nil { - return nil, err - } - } + allInsecure := false - webhookTransport.TLSClientConfig = clientTLSConfig + if os.Getenv("STEP_TLS_INSECURE") == "1" { + allInsecure = true + } + if !allInsecure { + var clientTLSConfig *tls.Config + if ca.opts.tlsConfig != nil { + // try using the tls Configuration supplied by the caller + log.Print("Using tls configuration supplied by the application") + tlsConfig = ca.opts.tlsConfig + clientTLSConfig = ca.opts.tlsConfig + } else { + // default to using the step-ca x509 Signer Interface + log.Print("Building new tls configuration using step-ca x509 Signer Interface") + tlsConfig, clientTLSConfig, err = ca.getTLSConfig(auth, cfg) + if err != nil { + return nil, err + } + } + webhookTransport.TLSClientConfig = clientTLSConfig + } // Using chi as the main router mux := chi.NewRouter() handler := http.Handler(mux) + var publicHandler http.Handler + var publicMux *chi.Mux + if cfg.PublicAddress != "" { + publicMux = chi.NewRouter() + publicHandler = http.Handler(publicMux) + } insecureMux := chi.NewRouter() insecureHandler := http.Handler(insecureMux) @@ -270,8 +327,18 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { mux.Route("/2.0/acme", func(r chi.Router) { acmeAPI.Route(r) }) - } + if cfg.PublicAddress != "" { + publicMux.Route("/acme", func(r chi.Router) { + acmeAPI.Route(r) + }) + // Use 2.0 because, at the moment, our ACME api is only compatible with v2.0 + // of the ACME spec. + publicMux.Route("/2.0/acme", func(r chi.Router) { + acmeAPI.Route(r) + }) + } + } // Admin API Router if cfg.AuthorityConfig.EnableAdmin { adminDB := auth.GetAdminDatabase() @@ -327,6 +394,9 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } handler = m.Middleware(handler) insecureHandler = m.Middleware(insecureHandler) + if cfg.PublicAddress != "" { + publicHandler = m.Middleware(publicHandler) + } } // Add logger if configured @@ -339,6 +409,9 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { legacyTraceHeader = logger.GetTraceHeader() handler = logger.Middleware(handler) insecureHandler = logger.Middleware(insecureHandler) + if cfg.PublicAddress != "" { + publicHandler = logger.Middleware(publicHandler) + } } // always use request ID middleware; traceHeader is provided for backwards compatibility (for now) @@ -346,12 +419,47 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { insecureHandler = requestid.New(legacyTraceHeader).Middleware(insecureHandler) // Create context with all the necessary values. - baseContext := buildContext(auth, scepAuthority, acmeDB, acmeLinker) + var eabClient pb.EABServiceClient + if cfg.ManagementHost != "" { + eabClient, err = eab.Connect(cfg.ManagementHost) + if err != nil { + return nil, errors.Wrap(err, "error connecting to EAB") + } + } + var validationBroker validation.MqttClient + if cfg.ValidationBroker != nil { + if cfg.ValidationBroker.Password == "" { + // pick password from env + cfg.ValidationBroker.Password = os.Getenv("MQTT_PASSWORD") + } + + validationBroker, err = acmeMqtt.Connect(acmeDB, cfg.ValidationBroker.Host, cfg.ValidationBroker.Username, cfg.ValidationBroker.Password, cfg.ValidationBroker.Organization) + if err != nil { + logrus.Warn("error connecting to validation broker. Only local validation will be available!") + } + } - ca.srv = server.New(cfg.Address, handler, tlsConfig) + baseContext := buildContext(auth, scepAuthority, acmeDB, acmeLinker, eabClient, validationBroker) + + if allInsecure { + ca.srv = server.New(cfg.Address, handler, nil) + } else { + ca.srv = server.New(cfg.Address, handler, tlsConfig) + } ca.srv.BaseContext = func(net.Listener) context.Context { return baseContext } + if cfg.PublicAddress != "" { + if allInsecure { + ca.public = server.New(cfg.PublicAddress, publicHandler, nil) + } else { + ca.public = server.New(cfg.PublicAddress, publicHandler, tlsConfig) + } + + ca.public.BaseContext = func(net.Listener) context.Context { + return baseContext + } + } // only start the insecure server if the insecure address is configured // and, currently, also only when it should serve SCEP endpoints. @@ -394,7 +502,7 @@ func (ca *CA) shouldServeInsecureServer() bool { } // buildContext builds the server base context. -func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB, acmeLinker acme.Linker) context.Context { +func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB, acmeLinker acme.Linker, eabClient pb.EABServiceClient, validationBroker validation.MqttClient) context.Context { ctx := authority.NewContext(context.Background(), a) if authDB := a.GetDatabase(); authDB != nil { ctx = db.NewContext(ctx, authDB) @@ -408,6 +516,12 @@ func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB if acmeDB != nil { ctx = acme.NewContext(ctx, acmeDB, acme.NewClient(), acmeLinker, nil) } + if eabClient != nil { + ctx = eab.NewContext(ctx, eabClient) + } + if validationBroker != nil { + ctx = validation.NewContext(ctx, validationBroker) + } return ctx } @@ -472,6 +586,13 @@ func (ca *CA) Run() error { defer wg.Done() errs <- ca.srv.ListenAndServe() }() + if ca.public != nil { + wg.Add(1) + go func() { + defer wg.Done() + errs <- ca.public.ListenAndServe() + }() + } // wait till error occurs; ensures the servers keep listening err := <-errs @@ -506,15 +627,25 @@ func (ca *CA) Stop() error { log.Printf("error stopping ca.Authority: %+v\n", err) } var insecureShutdownErr error + var publicErr error if ca.insecureSrv != nil { insecureShutdownErr = ca.insecureSrv.Shutdown() } + if ca.public != nil { + publicErr = ca.public.Shutdown() + } secureErr := ca.srv.Shutdown() - + err := ca.tp.Shutdown(context.Background()) + if err != nil { + return err + } if insecureShutdownErr != nil { return insecureShutdownErr } + if publicErr != nil { + return publicErr + } return secureErr } @@ -571,6 +702,12 @@ func (ca *CA) Reload() error { logContinue("Reload failed because server could not be replaced.") return errors.Wrap(err, "error reloading server") } + if ca.public != nil { + if err = ca.public.Reload(newCA.public); err != nil { + logContinue("Reload failed because server could not be replaced.") + return errors.Wrap(err, "error reloading server") + } + } // 1. Stop previous renewer // 2. Safely shutdown any internal resources (e.g. key manager) @@ -590,9 +727,17 @@ func (ca *CA) Reload() error { // get TLSConfig returns separate TLSConfigs for server and client with the // same self-renewing certificate. -func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config, error) { +func (ca *CA) getTLSConfig(auth *authority.Authority, cfg *config.Config) (*tls.Config, *tls.Config, error) { + + if cfg.Storage != "" { + err := os.Mkdir(cfg.Storage, 0600) + if err != nil && !os.IsExist(err) { + return nil, nil, errors.Wrap(err, "error creating storage directory") + } + } + // Create initial TLS certificate - tlsCrt, err := auth.GetTLSCertificate() + tlsCrt, err := auth.GetTLSCertificate(cfg.Storage, false) if err != nil { return nil, nil, err } @@ -603,7 +748,9 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config, ca.renewer.Stop() } - ca.renewer, err = NewTLSRenewer(tlsCrt, auth.GetTLSCertificate) + ca.renewer, err = NewTLSRenewer(tlsCrt, func() (*tls.Certificate, error) { + return auth.GetTLSCertificate(cfg.Storage, true) + }) if err != nil { return nil, nil, err } diff --git a/ca/tls_test.go b/ca/tls_test.go index d1ce11ea7..276c5856d 100644 --- a/ca/tls_test.go +++ b/ca/tls_test.go @@ -78,7 +78,7 @@ func startCATestServer(t *testing.T) *httptest.Server { ca, err := New(config) require.NoError(t, err) // Use a httptest.Server instead - baseContext := buildContext(ca.auth, nil, nil, nil) + baseContext := buildContext(ca.auth, nil, nil, nil, nil, nil) srv := startTestServer(baseContext, ca.srv.TLSConfig, ca.srv.Handler) return srv } diff --git a/cas/apiv1/options_test.go b/cas/apiv1/options_test.go index d48b63df8..891779938 100644 --- a/cas/apiv1/options_test.go +++ b/cas/apiv1/options_test.go @@ -12,15 +12,15 @@ type testCAS struct { name string } -func (t *testCAS) CreateCertificate(*CreateCertificateRequest) (*CreateCertificateResponse, error) { +func (t *testCAS) CreateCertificate(_ context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) { return nil, nil } -func (t *testCAS) RenewCertificate(*RenewCertificateRequest) (*RenewCertificateResponse, error) { +func (t *testCAS) RenewCertificate(_ context.Context, req *RenewCertificateRequest) (*RenewCertificateResponse, error) { return nil, nil } -func (t *testCAS) RevokeCertificate(*RevokeCertificateRequest) (*RevokeCertificateResponse, error) { +func (t *testCAS) RevokeCertificate(_ context.Context, req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { return nil, nil } diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 00ecc2a80..fe0932af7 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -1,6 +1,7 @@ package apiv1 import ( + "context" "crypto/x509" "net/http" "strings" @@ -9,9 +10,9 @@ import ( // CertificateAuthorityService is the interface implemented to support external // certificate authorities. type CertificateAuthorityService interface { - CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) - RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) - RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) + CreateCertificate(context context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) + RenewCertificate(context context.Context, req *RenewCertificateRequest) (*RenewCertificateResponse, error) + RevokeCertificate(context context.Context, req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } // CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService @@ -55,6 +56,8 @@ const ( VaultCAS = "vaultcas" // ExternalCAS is a CertificateAuthorityService using an external injected CA implementation ExternalCAS = "externalcas" + // SectigoCAS is a CertificateAuthorityService using sectigocas. + SectigoCAS = "sectigocas" ) // String returns a string from the type. It will always return the lower case diff --git a/cas/apiv1/services_test.go b/cas/apiv1/services_test.go index 2080f8431..619016523 100644 --- a/cas/apiv1/services_test.go +++ b/cas/apiv1/services_test.go @@ -1,18 +1,19 @@ package apiv1 import ( + "context" "testing" ) type simpleCAS struct{} -func (*simpleCAS) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { +func (*simpleCAS) CreateCertificate(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) { return nil, NotImplementedError{} } -func (*simpleCAS) RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) { +func (*simpleCAS) RenewCertificate(ctx context.Context, req *RenewCertificateRequest) (*RenewCertificateResponse, error) { return nil, NotImplementedError{} } -func (*simpleCAS) RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { +func (*simpleCAS) RevokeCertificate(ctx context.Context, req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { return nil, NotImplementedError{} } diff --git a/cas/cas_test.go b/cas/cas_test.go index 9fc06567a..7aa981e90 100644 --- a/cas/cas_test.go +++ b/cas/cas_test.go @@ -18,15 +18,15 @@ import ( type mockCAS struct{} -func (m *mockCAS) CreateCertificate(*apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { +func (m *mockCAS) CreateCertificate(ctx context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { panic("not implemented") } -func (m *mockCAS) RenewCertificate(*apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { +func (m *mockCAS) RenewCertificate(ctx context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { panic("not implemented") } -func (m *mockCAS) RevokeCertificate(*apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { +func (m *mockCAS) RevokeCertificate(ctx context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { panic("not implemented") } diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 398f7fed5..d946def95 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -193,7 +193,7 @@ func (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq } // CreateCertificate signs a new certificate using Google Cloud CAS. -func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { +func (c *CloudCAS) CreateCertificate(_ context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { switch { case req.Template == nil: return nil, errors.New("createCertificateRequest `template` cannot be nil") @@ -215,7 +215,7 @@ func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv // RenewCertificate renews the given certificate using Google Cloud CAS. // Google's CAS does not support the renew operation, so this method uses // CreateCertificate. -func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { +func (c *CloudCAS) RenewCertificate(_ context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { switch { case req.Template == nil: return nil, errors.New("renewCertificateRequest `template` cannot be nil") @@ -235,7 +235,7 @@ func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. } // RevokeCertificate revokes a certificate using Google Cloud CAS. -func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { +func (c *CloudCAS) RevokeCertificate(_ context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { reason, ok := revocationCodeMap[req.ReasonCode] switch { case !ok: diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 6e5d21339..484ec995d 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -549,7 +549,7 @@ func TestCloudCAS_CreateCertificate(t *testing.T) { client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } - got, err := c.CreateCertificate(tt.args.req) + got, err := c.CreateCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -665,7 +665,7 @@ func TestCloudCAS_RenewCertificate(t *testing.T) { client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } - got, err := c.RenewCertificate(tt.args.req) + got, err := c.RenewCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -744,7 +744,7 @@ func TestCloudCAS_RevokeCertificate(t *testing.T) { client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } - got, err := c.RevokeCertificate(tt.args.req) + got, err := c.RevokeCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/cas/sectigocas/eab/client.go b/cas/sectigocas/eab/client.go new file mode 100644 index 000000000..ce5b9f14c --- /dev/null +++ b/cas/sectigocas/eab/client.go @@ -0,0 +1,49 @@ +package eab + +import ( + "context" + + pb "github.com/hm-edu/portal-apis" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type eabKey struct{} + +// NewContext adds the given eab client to the context. +func NewContext(ctx context.Context, a pb.EABServiceClient) context.Context { + return context.WithValue(ctx, eabKey{}, a) +} + +// FromContext returns the eab client from the given context. +func FromContext(ctx context.Context) (a pb.EABServiceClient, ok bool) { + a, ok = ctx.Value(eabKey{}).(pb.EABServiceClient) + return +} + +// MustFromContext returns the eab client from the given context. It will +// panic if no eab client is not in the context. +func MustFromContext(ctx context.Context) pb.EABServiceClient { + if a, ok := FromContext(ctx); !ok { + panic("eab client is not in the context") + } else { + return a + } +} + +func Connect(host string) (pb.EABServiceClient, error) { + + conn, err := grpc.NewClient( + host, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + ) + if err != nil { + return nil, err + } + + apiClient := pb.NewEABServiceClient(conn) + return apiClient, nil + +} diff --git a/cas/sectigocas/sectigocas.go b/cas/sectigocas/sectigocas.go new file mode 100644 index 000000000..d77c8f3cf --- /dev/null +++ b/cas/sectigocas/sectigocas.go @@ -0,0 +1,180 @@ +package sectigocas + +import ( + "context" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/acme/api" + "github.com/smallstep/certificates/authority/provisioner" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + + pb "github.com/hm-edu/portal-apis" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/smallstep/certificates/cas/apiv1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type Options struct { + PKIBackend string `json:"pkiBackend"` + EABBackend string `json:"eabBackend"` +} + +func init() { + apiv1.Register(apiv1.SectigoCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) { + return New(ctx, opts) + }) +} + +func New(ctx context.Context, opts apiv1.Options) (*SectigoCAS, error) { + var config Options + err := json.Unmarshal(opts.Config, &config) + if err != nil { + return nil, err + } + + conn, err := grpc.NewClient( + config.PKIBackend, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + ) + if err != nil { + return nil, err + } + sslServiceClient := pb.NewSSLServiceClient(conn) + conn, err = grpc.NewClient( + config.EABBackend, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + ) + if err != nil { + return nil, err + } + eabClient := pb.NewEABServiceClient(conn) + + return &SectigoCAS{sslServiceClient: sslServiceClient, eabClient: eabClient, logger: logrus.StandardLogger()}, nil +} + +type SectigoCAS struct { + sslServiceClient pb.SSLServiceClient + eabClient pb.EABServiceClient + logger *logrus.Logger +} + +func parseCertificates(cert []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for block, rest := pem.Decode(cert); block != nil; block, rest = pem.Decode(rest) { + switch block.Type { + case "CERTIFICATE": + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, cert) + default: + return nil, errors.New("Unknown entry in cert chain") + } + } + return certs, nil +} +func accountFromContext(ctx context.Context) *acme.Account { + val, ok := ctx.Value(api.AccContextKey).(*acme.Account) + if !ok || val == nil { + return nil + } + return val +} + +func (s *SectigoCAS) signCertificate(ctx context.Context, cr *x509.CertificateRequest) (*x509.Certificate, []*x509.Certificate, error) { + sans := make([]string, 0, len(cr.DNSNames)+len(cr.EmailAddresses)+len(cr.IPAddresses)+len(cr.URIs)) + sans = append(sans, cr.DNSNames...) + for _, ip := range cr.IPAddresses { + sans = append(sans, ip.String()) + } + for _, u := range cr.URIs { + sans = append(sans, u.String()) + } + + issuer := "" + prov, ok := acme.ProvisionerFromContext(ctx) + if !ok || prov == nil { + issuer = "Internal" + } else { + acmeProv, ok := prov.(*provisioner.ACME) + if !ok || acmeProv == nil { + return nil, nil, errors.New("No ACME provisioner passed!") + } + if acmeProv.RequireEAB { + acc := accountFromContext(ctx) + if acc == nil { + return nil, nil, errors.New("No account passed!") + } + user, err := s.eabClient.ResolveAccountId(context.Background(), &pb.ResolveAccountIdRequest{AccountId: acc.ID}) + if err != nil { + return nil, nil, errors.WithMessage(err, "Error resolving user account!") + } + issuer = fmt.Sprintf("%v (EAB: %v)", user.User, user.EabKey) + } + + } + + certificates, err := s.sslServiceClient.IssueCertificate(context.Background(), &pb.IssueSslRequest{ + Issuer: issuer, + SubjectAlternativeNames: sans, + Source: "ACME", + Csr: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: cr.Raw})), + }) + if err != nil { + s.logger.WithField("error", err).Error("Failed to sign certificate") + return nil, nil, err + } + certs, err := parseCertificates([]byte(certificates.Certificate)) + if err != nil { + s.logger.WithField("error", err).Error("Failed to parse certificate") + return nil, nil, err + } + return certs[0], certs[1:], nil +} + +func (s *SectigoCAS) CreateCertificate(ctx context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { + cert, chain, err := s.signCertificate(ctx, req.CSR) + if err != nil { + return nil, err + } + return &apiv1.CreateCertificateResponse{ + Certificate: cert, + CertificateChain: chain, + }, nil +} + +func (s *SectigoCAS) RenewCertificate(ctx context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { + cert, chain, err := s.signCertificate(ctx, req.CSR) + if err != nil { + return nil, err + } + return &apiv1.RenewCertificateResponse{ + Certificate: cert, + CertificateChain: chain, + }, nil +} + +func (s *SectigoCAS) RevokeCertificate(ctx context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + _, err := s.sslServiceClient.RevokeCertificate(context.Background(), &pb.RevokeSslRequest{ + Identifier: &pb.RevokeSslRequest_Serial{Serial: req.SerialNumber}, + Reason: req.Reason, + }) + if err != nil { + s.logger.WithField("error", err).Error("Failed to revoke certificate") + return nil, err + } + + return &apiv1.RevokeCertificateResponse{ + Certificate: req.Certificate, + CertificateChain: nil, + }, nil +} diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index dd961975a..a0662fa50 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -59,7 +59,7 @@ func (c *SoftCAS) Type() apiv1.Type { } // CreateCertificate signs a new certificate using Golang or KMS crypto. -func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { +func (c *SoftCAS) CreateCertificate(_ context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { switch { case req.Template == nil: return nil, errors.New("createCertificateRequest `template` cannot be nil") @@ -95,7 +95,7 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 } // RenewCertificate signs the given certificate template using Golang or KMS crypto. -func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { +func (c *SoftCAS) RenewCertificate(_ context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { switch { case req.Template == nil: return nil, errors.New("createCertificateRequest `template` cannot be nil") @@ -127,7 +127,7 @@ func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R // RevokeCertificate revokes the given certificate in step-ca. In SoftCAS this // operation is a no-op as the actual revoke will happen when we store the entry // in the db. -func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { +func (c *SoftCAS) RevokeCertificate(_ context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { chain, _, err := c.getCertSigner() if err != nil { return nil, err diff --git a/cas/softcas/softcas_test.go b/cas/softcas/softcas_test.go index 8c04de3af..d2e67875d 100644 --- a/cas/softcas/softcas_test.go +++ b/cas/softcas/softcas_test.go @@ -356,7 +356,7 @@ func TestSoftCAS_CreateCertificate(t *testing.T) { Signer: tt.fields.Signer, CertificateSigner: tt.fields.CertificateSigner, } - got, err := c.CreateCertificate(tt.args.req) + got, err := c.CreateCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("SoftCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -400,7 +400,7 @@ func TestSoftCAS_CreateCertificate_pss(t *testing.T) { CertificateChain: []*x509.Certificate{iss}, Signer: signer, } - cert, err := c.CreateCertificate(&apiv1.CreateCertificateRequest{ + cert, err := c.CreateCertificate(context.Background(), &apiv1.CreateCertificateRequest{ Template: &x509.Certificate{ Subject: pkix.Name{CommonName: "test.smallstep.com"}, DNSNames: []string{"test.smallstep.com"}, @@ -486,7 +486,7 @@ func TestSoftCAS_CreateCertificate_ec_rsa(t *testing.T) { CertificateChain: []*x509.Certificate{iss}, Signer: intSigner, } - cert, err := c.CreateCertificate(&apiv1.CreateCertificateRequest{ + cert, err := c.CreateCertificate(context.Background(), &apiv1.CreateCertificateRequest{ Template: &x509.Certificate{ Subject: pkix.Name{CommonName: "test.smallstep.com"}, DNSNames: []string{"test.smallstep.com"}, @@ -584,7 +584,7 @@ func TestSoftCAS_RenewCertificate(t *testing.T) { Signer: tt.fields.Signer, CertificateSigner: tt.fields.CertificateSigner, } - got, err := c.RenewCertificate(tt.args.req) + got, err := c.RenewCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("SoftCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -652,7 +652,7 @@ func TestSoftCAS_RevokeCertificate(t *testing.T) { Signer: tt.fields.Signer, CertificateSigner: tt.fields.CertificateSigner, } - got, err := c.RevokeCertificate(tt.args.req) + got, err := c.RevokeCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("SoftCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index cab8f2031..30febb2c9 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -72,7 +72,7 @@ func (s *StepCAS) Type() apiv1.Type { // CreateCertificate uses the step-ca sign request with the configured // provisioner to get a new certificate from the certificate authority. -func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { +func (s *StepCAS) CreateCertificate(ctx context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { switch { case req.CSR == nil: return nil, errors.New("createCertificateRequest `csr` cannot be nil") @@ -107,7 +107,7 @@ func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 // RenewCertificate will always return a non-implemented error as mTLS renewals // are not supported yet. -func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { +func (s *StepCAS) RenewCertificate(ctx context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { if req.Token == "" { return nil, apiv1.ValidationError{Message: "renewCertificateRequest `token` cannot be empty"} } @@ -130,7 +130,7 @@ func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R } // RevokeCertificate revokes a certificate. -func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { +func (s *StepCAS) RevokeCertificate(ctx context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { if req.SerialNumber == "" && req.Certificate == nil { return nil, errors.New("revokeCertificateRequest `serialNumber` or `certificate` are required") } diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index d2846fb01..f191958fc 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -768,7 +768,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) { authorityID: "authority-id", fingerprint: tt.fields.fingerprint, } - got, err := s.CreateCertificate(tt.args.req) + got, err := s.CreateCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("StepCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -833,7 +833,7 @@ func TestStepCAS_RenewCertificate(t *testing.T) { client: tt.fields.client, fingerprint: tt.fields.fingerprint, } - got, err := s.RenewCertificate(tt.args.req) + got, err := s.RenewCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("StepCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -933,7 +933,7 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { client: tt.fields.client, fingerprint: tt.fields.fingerprint, } - got, err := s.RevokeCertificate(tt.args.req) + got, err := s.RevokeCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("StepCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index 16adccc4e..63ceabd5a 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -116,7 +116,7 @@ func (v *VaultCAS) Type() apiv1.Type { } // CreateCertificate signs a new certificate using Hashicorp Vault. -func (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { +func (v *VaultCAS) CreateCertificate(_ context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { switch { case req.CSR == nil: return nil, errors.New("createCertificate `csr` cannot be nil") @@ -172,12 +172,12 @@ func (v *VaultCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest // RenewCertificate will always return a non-implemented error as renewals // are not supported yet. -func (v *VaultCAS) RenewCertificate(*apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { +func (v *VaultCAS) RenewCertificate(_ context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { return nil, apiv1.NotImplementedError{Message: "vaultCAS does not support renewals"} } // RevokeCertificate revokes a certificate by serial number. -func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { +func (v *VaultCAS) RevokeCertificate(_ context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { if req.SerialNumber == "" && req.Certificate == nil { return nil, errors.New("revokeCertificate `serialNumber` or `certificate` are required") } diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 2f44bc059..4b1c1711b 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -273,7 +273,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { client: tt.fields.client, config: tt.fields.options, } - got, err := c.CreateCertificate(tt.args.req) + got, err := c.CreateCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("VaultCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -396,7 +396,7 @@ func TestVaultCAS_RevokeCertificate(t *testing.T) { client: tt.fields.client, config: tt.fields.options, } - got, err := s.RevokeCertificate(tt.args.req) + got, err := s.RevokeCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("VaultCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) return @@ -446,7 +446,7 @@ func TestVaultCAS_RenewCertificate(t *testing.T) { client: tt.fields.client, config: tt.fields.options, } - got, err := s.RenewCertificate(tt.args.req) + got, err := s.RenewCertificate(context.TODO(), tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("VaultCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/cmd/step-agent/main.go b/cmd/step-agent/main.go new file mode 100644 index 000000000..85d322a25 --- /dev/null +++ b/cmd/step-agent/main.go @@ -0,0 +1,179 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "os/signal" + "strings" + "syscall" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/sirupsen/logrus" + "github.com/smallstep/certificates/acme" + "github.com/smallstep/certificates/acme/validation" + "github.com/urfave/cli" + "go.step.sm/cli-utils/step" + "go.step.sm/cli-utils/ui" +) + +var agent = cli.Command{ + Name: "agent", + Usage: "start the step-ca agent", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "host", + Usage: "the host of the mqtt broker", + }, + cli.StringFlag{ + Name: "user", + Usage: "the user for the mqtt broker", + }, + cli.StringFlag{ + Name: "password", + Usage: "the password for the mqtt broker", + }, + cli.StringFlag{ + Name: "organization", + Usage: "the organization for the mqtt broker connection", + }, + }, + + Action: func(c *cli.Context) error { + options := mqtt.NewClientOptions() + options.SetOrderMatters(false) + options.ConnectTimeout = time.Second + options.WriteTimeout = time.Second + options.KeepAlive = 10 + options.PingTimeout = time.Second + options.ConnectRetry = true + options.AutoReconnect = true + options.ClientID = fmt.Sprintf("acme-agent-%s-%d", c.String("organization"), time.Now().UnixNano()) + options.Username = c.String("user") + options.Password = c.String("password") + options.AddBroker(fmt.Sprintf("ssl://%s:8883", c.String("host"))) + logrus.Infof("connecting to mqtt broker") + + // Establish connection to MQTT broker + options.OnConnectionLost = func(cl mqtt.Client, err error) { + logrus.Println("mqtt connection lost") + } + options.OnConnect = func(cl mqtt.Client) { + logrus.Println("mqtt connection established") + // Subscribe to topic + token := cl.Subscribe(fmt.Sprintf("%s/jobs", c.String("organization")), 0, func(client mqtt.Client, msg mqtt.Message) { + logrus.Infof("received message on topic %s", msg.Topic()) + logrus.Infof("message: %s", msg.Payload()) + + var data validation.ValidationRequest + + req := msg.Payload() + json.Unmarshal(req, &data) + + logger := logrus.WithField("authz", data.Authz).WithField("target", data.Target).WithField("account", data.Challenge) + + http := acme.NewClient() + resp, err := http.Get(data.Target) + if err != nil { + logger.WithError(err).Warn("validating failed") + return + } + + defer resp.Body.Close() + if resp.StatusCode >= 400 { + logger.Warnf("validation for %s failed with error: %s", data.Target, resp.Status) + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + logger.WithError(err).Warn("parsing body failed") + return + } + + keyAuth := strings.TrimSpace(string(body)) + logger.Infof("keyAuth: %s", keyAuth) + + json, err := json.Marshal(&validation.ValidationResponse{ + Authz: data.Authz, + Challenge: data.Challenge, + Content: keyAuth, + }) + if err != nil { + logger.WithError(err).Warn("marshalling failed") + return + } + // Publish to topic + token := cl.Publish(fmt.Sprintf("%s/data", c.String("organization")), 0, false, json) + if token.WaitTimeout(30*time.Second) && token.Error() != nil { + logger.WithError(token.Error()).Warn("publishing failed") + } else { + logger.Infof("published to topic %s", fmt.Sprintf("%s/data", c.String("organization"))) + } + + }) + + if token.WaitTimeout(30*time.Second) && token.Error() != nil { + logrus.WithError(token.Error()).Warn("subscribing failed") + } else { + logrus.Infof("subscribed to topic %s", fmt.Sprintf("%s/jobs", c.String("organization"))) + } + } + + options.OnReconnecting = func(mqtt.Client, *mqtt.ClientOptions) { + logrus.Println("mqtt reconnecting") + } + + client := mqtt.NewClient(options) + if token := client.Connect(); token.WaitTimeout(30*time.Second) && token.Error() != nil { + logrus.Warn(token.Error()) + } + + return nil + }, +} + +// commit and buildTime are filled in during build by the Makefile +var ( + BuildTime = "N/A" + Version = "N/A" +) + +func init() { + step.Set("Smallstep Agent", Version, BuildTime) +} + +func exit(code int) { + ui.Reset() + os.Exit(code) +} + +func main() { + ui.Init() + app := cli.NewApp() + app.Name = "step-agent" + app.Usage = "step-agent" + app.Version = step.Version() + app.Action = func(c *cli.Context) error { + return agent.Run(c) + } + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + done := make(chan bool, 1) + + go func() { + <-sigs + done <- true + }() + + if err := app.Run(os.Args); err != nil { + logrus.Warn(err) + exit(1) + } + + <-done + exit(0) +} diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index c7ece08ff..5206b6b02 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -38,6 +38,7 @@ import ( // Enabled cas interfaces. _ "github.com/smallstep/certificates/cas/cloudcas" + _ "github.com/smallstep/certificates/cas/sectigocas" _ "github.com/smallstep/certificates/cas/softcas" _ "github.com/smallstep/certificates/cas/stepcas" _ "github.com/smallstep/certificates/cas/vaultcas" diff --git a/db/db.go b/db/db.go index 503a7c14b..346292e97 100644 --- a/db/db.go +++ b/db/db.go @@ -60,6 +60,7 @@ type AuthDB interface { IsSSHHost(name string) (bool, error) GetSSHHostPrincipals() ([]string, error) Shutdown() error + Ping() error } type dbKey struct{} @@ -525,6 +526,11 @@ func (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error { return m.Err } +// Ping mock +func (m *MockAuthDB) Ping() error { + return nil +} + // IsRevoked mock. func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { if m.MIsRevoked != nil { @@ -643,6 +649,11 @@ type MockNoSQLDB struct { MCmpAndSwap func(bucket, key, old, newval []byte) ([]byte, bool, error) } +// Ping mock +func (m *MockNoSQLDB) Ping() error { + return nil +} + // CmpAndSwap mock func (m *MockNoSQLDB) CmpAndSwap(bucket, key, old, newval []byte) ([]byte, bool, error) { if m.MCmpAndSwap != nil { diff --git a/db/simple.go b/db/simple.go index dbef2d615..078802177 100644 --- a/db/simple.go +++ b/db/simple.go @@ -26,6 +26,11 @@ func newSimpleDB(*Config) (*SimpleDB, error) { return db, nil } +// Ping noop +func (s *SimpleDB) Ping() error { + return nil +} + // IsRevoked noop func (s *SimpleDB) IsRevoked(string) (bool, error) { return false, nil diff --git a/docker/Dockerfile.agent b/docker/Dockerfile.agent new file mode 100644 index 000000000..3d9ebe4b0 --- /dev/null +++ b/docker/Dockerfile.agent @@ -0,0 +1,18 @@ +FROM golang:alpine AS builder + +WORKDIR /src +COPY . . + +RUN apk add --no-cache curl git make libcap +RUN PKG=github.com/smallstep/certificates/cmd/step-agent BINNAME=step-agent make V=1 bin/step-agent +RUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-agent + +FROM smallstep/step-cli:latest + +COPY --from=builder /src/bin/step-agent /usr/local/bin/step-agent + +USER step + +VOLUME ["/home/step"] +STOPSIGNAL SIGTERM +CMD exec /usr/local/bin/step-agent diff --git a/go.mod b/go.mod index 4c83e83ea..ebad7c813 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( cloud.google.com/go/longrunning v0.5.7 cloud.google.com/go/security v1.16.1 github.com/Masterminds/sprig/v3 v3.2.3 + github.com/ThalesIgnite/crypto11 v1.2.5 // indirect github.com/dgraph-io/badger v1.6.2 github.com/dgraph-io/badger/v2 v2.2007.4 + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/fxamacker/cbor/v2 v2.6.0 github.com/go-chi/chi/v5 v5.0.12 github.com/go-jose/go-jose/v3 v3.0.3 @@ -19,6 +21,9 @@ require ( github.com/hashicorp/vault/api v1.13.0 github.com/hashicorp/vault/api/auth/approle v0.6.0 github.com/hashicorp/vault/api/auth/kubernetes v0.6.0 + github.com/hm-edu/portal-apis v0.0.0-20230929065638-ad1f7e7c7ab3 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/newrelic/go-agent/v3 v3.33.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.0 @@ -51,6 +56,19 @@ require ( cloud.google.com/go/iam v1.1.7 // indirect cloud.google.com/go/kms v1.15.8 // indirect filippo.io/edwards25519 v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 + go.opentelemetry.io/contrib/propagators/b3 v1.24.0 + go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel/sdk v1.26.0 +) + +require ( + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 +) + +require ( github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 // indirect @@ -59,8 +77,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.0 // indirect - github.com/ThalesIgnite/crypto11 v1.2.5 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect @@ -76,13 +93,13 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect github.com/aws/smithy-go v1.20.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cenkalti/backoff/v3 v3.2.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -105,17 +122,18 @@ require ( github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/huandu/xstrings v1.3.3 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -124,11 +142,9 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/klauspost/compress v1.16.3 // indirect + github.com/klauspost/compress v1.17.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -144,18 +160,17 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/schollz/jsonstore v1.1.0 // indirect - github.com/shopspring/decimal v1.2.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/bbolt v1.3.9 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect @@ -166,3 +181,10 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +require ( + github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/gorilla/websocket v1.5.0 // indirect +) + +replace github.com/smallstep/nosql => github.com/hm-edu/nosql v0.4.1-0.20230305071512-139857866201 diff --git a/go.sum b/go.sum index e9db4c9e5..c53ec08f6 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,9 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= @@ -77,8 +78,11 @@ github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -116,8 +120,8 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw 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.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -126,6 +130,8 @@ github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5O github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -135,6 +141,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= @@ -237,6 +245,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -250,17 +262,20 @@ github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= +github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.12.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= @@ -270,11 +285,16 @@ github.com/hashicorp/vault/api/auth/approle v0.6.0 h1:ELfFFQlTM/e97WJKu1HvNFa7lQ github.com/hashicorp/vault/api/auth/approle v0.6.0/go.mod h1:CCoIl1xBC3lAWpd1HV+0ovk76Z8b8Mdepyk21h3pGk0= github.com/hashicorp/vault/api/auth/kubernetes v0.6.0 h1:K8sKGhtTAqGKfzaaYvUSIOAqTOIn3Gk1EsCEAMzZHtM= github.com/hashicorp/vault/api/auth/kubernetes v0.6.0/go.mod h1:Htwcjez5J9PwAHaZ1EYMBlgGq3/in5ajUV4+WCPihPE= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/hm-edu/nosql v0.4.1-0.20230305071512-139857866201 h1:KB8SVIw1MA30wUUXYziiTErSw487ahokcesqzgPlK/o= +github.com/hm-edu/nosql v0.4.1-0.20230305071512-139857866201/go.mod h1:jOXwLtockXORUPPZ2MCUcIkGR6w0cN1QGZniY9DITQA= +github.com/hm-edu/portal-apis v0.0.0-20230929065638-ad1f7e7c7ab3 h1:XrnVZ89D8r7kXCmPsPCa+MtjZJvhhqT1Bkix5iH0sRQ= +github.com/hm-edu/portal-apis v0.0.0-20230929065638-ad1f7e7c7ab3/go.mod h1:o2FYTwt6w4uXfIoPAFRQpxbOOhGjde0PiGZlErVVyZk= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -325,8 +345,8 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= -github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -425,8 +445,9 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -439,8 +460,6 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y= -github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y= github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg= github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw= @@ -451,8 +470,8 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -494,14 +513,24 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/contrib/propagators/b3 v1.24.0 h1:n4xwCdTx3pZqZs2CjS/CUZAs03y3dZcGhC/FepKtEUY= +go.opentelemetry.io/contrib/propagators/b3 v1.24.0/go.mod h1:k5wRxKRU2uXx2F8uNJ4TaonuEO/V7/5xoz7kdsDACT8= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= go.step.sm/crypto v0.44.8 h1:jDSHL6FdB1UTA0d56ECNx9XtLVkewzeg38Vy3HWB3N8= @@ -512,6 +541,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -605,6 +636,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/logging/handler.go b/logging/handler.go index 701d631ae..5b77d933a 100644 --- a/logging/handler.go +++ b/logging/handler.go @@ -72,6 +72,13 @@ func (l *LoggerHandler) writeEntry(w ResponseLogger, r *http.Request, t time.Tim addr = r.RemoteAddr } + // Try to get X-Forwarded-For header first + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + addr = strings.Split(xff, ", ")[0] + } else if xrip := r.Header.Get("X-Real-Ip"); xrip != "" { + addr = xrip + } + // From https://github.com/gorilla/handlers uri := r.RequestURI // Requests using the CONNECT method over HTTP/2.0 must use diff --git a/logging/logger.go b/logging/logger.go index 1716a7f4c..30bb504e5 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -61,6 +61,7 @@ func New(name string, raw json.RawMessage) (*Logger, error) { } if formatter != nil { logger.Formatter = formatter + logrus.SetFormatter(formatter) } return logger, nil } diff --git a/server/server.go b/server/server.go index e12c792c6..829b99337 100644 --- a/server/server.go +++ b/server/server.go @@ -42,7 +42,7 @@ func newHTTPServer(addr string, handler http.Handler, tlsConfig *tls.Config) *ht Addr: addr, Handler: handler, TLSConfig: tlsConfig, - WriteTimeout: 15 * time.Second, + WriteTimeout: 10 * time.Minute, ReadTimeout: 15 * time.Second, ReadHeaderTimeout: 15 * time.Second, IdleTimeout: 15 * time.Second, diff --git a/test/integration/scep/common_test.go b/test/integration/scep/common_test.go index 86f64c3d2..c9f27be9e 100644 --- a/test/integration/scep/common_test.go +++ b/test/integration/scep/common_test.go @@ -243,7 +243,7 @@ type testCAS struct { ca *minica.CA } -func (c *testCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { +func (c *testCAS) CreateCertificate(ctx context.Context, req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { cert, err := c.ca.SignCSR(req.CSR) if err != nil { return nil, fmt.Errorf("failed signing CSR: %w", err) @@ -254,11 +254,11 @@ func (c *testCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 CertificateChain: []*x509.Certificate{cert, c.ca.Intermediate}, }, nil } -func (c *testCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { +func (c *testCAS) RenewCertificate(ctx context.Context, req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { return nil, errors.New("not implemented") } -func (c *testCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { +func (c *testCAS) RevokeCertificate(ctx context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { return nil, errors.New("not implemented") }