diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 752628259..7b1b5c30f 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -14,5 +14,5 @@ jobs: - name: Run code spelling check uses: codespell-project/actions-codespell@v2 with: - ignore_words_list: allos,ans,dne,noe,referr,ssudo,te,tranfer,ue - skip: go.*,zititest/go.*,./controller/storage/zitiql/zitiql_parser.go + ignore_words_list: actieve,allos,ans,dne,noe,referr,ssudo,te,tranfer,ue + skip: go.*,zititest/go.*,./controller/storage/zitiql/zitiql_parser.go,*.pem,*.cert,*.key diff --git a/.github/workflows/mattermost-channel-posts.yml b/.github/workflows/mattermost-channel-posts.yml index e707585f4..4a88c8d01 100644 --- a/.github/workflows/mattermost-channel-posts.yml +++ b/.github/workflows/mattermost-channel-posts.yml @@ -21,9 +21,16 @@ jobs: send-notifications: runs-on: ubuntu-24.04 name: POST Webhook with Python + # a chat notification must never fail the release: this job's status is reported as success even when the + # Mattermost webhook is unreachable, so workflows that gate on the checks for a revision aren't blocked by it + continue-on-error: true + # the webhook is dialed over the ziti overlay with no client-side connect timeout, so cap the job instead + timeout-minutes: 5 if: github.actor != 'dependabot[bot]' steps: + # per-step too, so an unreachable webhook in the first post doesn't skip the second - uses: openziti/ziti-mattermost-action-py@v1 + continue-on-error: true if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') @@ -35,6 +42,7 @@ jobs: senderUsername: "GitHubZ" - uses: openziti/ziti-mattermost-action-py@v1 + continue-on-error: true if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') diff --git a/.github/workflows/mattermost-webhook.yml b/.github/workflows/mattermost-webhook.yml index dfa1cbd8b..7379b19d1 100644 --- a/.github/workflows/mattermost-webhook.yml +++ b/.github/workflows/mattermost-webhook.yml @@ -14,6 +14,9 @@ on: jobs: mattermost-ziti-nodejs-webhook: continue-on-error: true + # the webhook is dialed over the ziti overlay with no client-side connect timeout; when the service is down this + # job has sat for hours, stalling anything that waits on the checks for a revision + timeout-minutes: 5 runs-on: ubuntu-24.04 name: POST Webhook with NodeJS if: github.repository_owner == 'openziti' && github.actor != 'dependabot[bot]' diff --git a/.github/workflows/promote-downstreams.yml b/.github/workflows/promote-downstreams.yml index 0d8dacc5e..0a17bdb6a 100644 --- a/.github/workflows/promote-downstreams.yml +++ b/.github/workflows/promote-downstreams.yml @@ -1,61 +1,26 @@ name: Promote Downstream Releases -on: - # may be triggered manually on a release tag that represents a prerelease to promote it to a release in the downstream package repositories and Docker Hub +on: + # may be triggered manually to promote a release tag to stable in the downstream package repositories and Docker Hub. + # Dispatch from the tag itself, or from any branch and set the 'tag' input to the release tag to promote. workflow_dispatch: + inputs: + tag: + description: Release tag to promote, e.g. v2.0.0. Defaults to the ref the run was dispatched from; set this to promote a specific tag when dispatching from a branch. + required: false + type: string # GitHub release is marked stable, i.e., isPrerelease: false release: types: [released] # this release event activity type excludes prereleases -# cancel older, redundant runs of same workflow on same branch +# cancel older, redundant runs of same workflow for the same tag (or dispatched ref) concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.head_ref || github.ref_name }} cancel-in-progress: true jobs: - wait_for_release: - name: Wait for Release Builds to Succeed - runs-on: ubuntu-24.04 - steps: - - name: Debug action - uses: hmarr/debug-action@v3 - - - name: Wait for all checks on this rev - uses: lewagon/wait-on-check-action@v1.5.0 - with: - ref: ${{ github.ref_name }} - repo-token: ${{ secrets.GITHUB_TOKEN }} - # seconds between polling the checks api for job statuses - wait-interval: 30 - # confusingly, this means "pause this step until all jobs from all workflows in same run have completed" - running-workflow-name: Wait for Release Builds to Succeed - # comma-separated list of check names (job..name) to ignore - ignore-checks: SDK Terminator Validation,Fablab HA Smoketest,POST Webhook,Release Quickstart Job,Parse Tag Regex - - - name: Git Checkout - if: failure() - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Diagnose Failed "Wait for Release Builds to Succeed" - if: failure() - shell: bash - run: | - set -o pipefail - set -o xtrace - - COMMIT_SHA=$(git rev-parse ${GITHUB_REF_NAME}^{commit}) - for STATUS in cancelled failure - do - gh run list --repo "${GITHUB_REPOSITORY}" --status "${STATUS}" --commit "${COMMIT_SHA}" - done - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # the purpose of this job is to enforce that the Git ref promoted is a semver eligible for stable release, i.e., not having a semver pre-release suffix; the extracted version without the leading 'v' is passed to the docker job as the container image tag parse_version: - needs: wait_for_release name: Parse Tag Regex runs-on: ubuntu-24.04 outputs: @@ -65,11 +30,14 @@ jobs: - name: Validate the Release Tag is a Stable Release Ref id: validate shell: bash + env: + # the 'tag' input when dispatched from a branch, otherwise the ref the run fired on (the tag) + PROMOTE_REF: ${{ github.event.inputs.tag || github.ref_name }} run: | - if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "version=${GITHUB_REF_NAME#v}" | tee -a $GITHUB_OUTPUT + if [[ "${PROMOTE_REF}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "version=${PROMOTE_REF#v}" | tee -a $GITHUB_OUTPUT else - echo "${GITHUB_REF_NAME} is not a semver stable release ref" >&2 + echo "${PROMOTE_REF} is not a semver stable release ref" >&2 exit 1 fi @@ -96,14 +64,15 @@ jobs: | sort -V \ | tail -1 ) - CURRENT_VERSION="${GITHUB_REF_NAME}" - + CURRENT_VERSION="${PROMOTE_REF}" + if [[ "$CURRENT_VERSION" == "$HIGHEST_VERSION" ]]; then echo "highest=true" | tee -a $GITHUB_OUTPUT else echo "highest=false" | tee -a $GITHUB_OUTPUT fi env: + PROMOTE_REF: ${{ github.event.inputs.tag || github.ref_name }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} promote_docker: @@ -137,7 +106,9 @@ jobs: name: Promote ${{ matrix.package_name }}-${{ matrix.arch.rpm }}.${{ matrix.packager }} needs: parse_version strategy: - fail-fast: true + # each copy is independent and idempotent, so let the rest finish and report exactly which one failed instead of + # cancelling siblings and leaving an arbitrary subset of the packages promoted + fail-fast: false matrix: package_name: - openziti @@ -161,7 +132,7 @@ jobs: ZITI_RPM_PROD_REPO: ${{ vars.ZITI_RPM_PROD_REPO || 'zitipax-openziti-rpm-stable' }} steps: - name: Configure jFrog CLI - uses: jfrog/setup-jfrog-cli@v4 + uses: jfrog/setup-jfrog-cli@v5 env: JF_ENV_1: ${{ secrets.ZITI_ARTIFACTORY_CLI_CONFIG_PACKAGE_UPLOAD }} diff --git a/.github/workflows/publish-linux-packages.yml b/.github/workflows/publish-linux-packages.yml index d4bdc6300..0059ab098 100644 --- a/.github/workflows/publish-linux-packages.yml +++ b/.github/workflows/publish-linux-packages.yml @@ -88,7 +88,7 @@ jobs: if-no-files-found: error - name: Configure jFrog CLI - uses: jfrog/setup-jfrog-cli@v4 + uses: jfrog/setup-jfrog-cli@v5 env: JF_ENV_1: ${{ secrets.ZITI_ARTIFACTORY_CLI_CONFIG_PACKAGE_UPLOAD }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a79375f8..bdaefeffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Release 2.0.2 +# Release 2.0.4 ## What's New @@ -6,10 +6,116 @@ ## Component Updates and Bug Fixes -* github.com/openziti/ziti/v2: [v2.0.1 -> v2.0.2](https://github.com/openziti/ziti/compare/v2.0.0...v2.0.1) +* github.com/openziti/ziti/v2: [v2.0.3 -> v2.0.4](https://github.com/openziti/ziti/compare/v2.0.3...v2.0.4) + * [Issue #4264](https://github.com/openziti/ziti/issues/4264) - [Backport-2.0] Router control-channel connect/disconnect race can leave a reconnected router de-registered + * [Issue #3891](https://github.com/openziti/ziti/issues/3891) - [Backport-2.0] OIDC auth fails when the controller's server certificate has a wildcard SAN. A wildcard SAN is now expanded to the exact hostnames listed in the new `edge-oidc` `allowedHostnames` option, which become valid OIDC issuers + +# Release 2.0.3 + +## What's New + +* Security fixes (see Security Advisories below) +* Bug fixes +* Controller read throughput under load: this release picks up bbolt v1.5.0, which removes a linear + scan over all open read transactions that ran while holding bbolt's single global transaction + mutex. Every controller read transaction takes that mutex twice, on open and on close, so the scan + cost grew with read concurrency and could put a controller serving a high rate of service-list and + policy queries into a lock convoy: many goroutines waiting on one mutex, a machine that looks + fully busy while little work completes, and timeouts unexplained by the actual workload. + +## Security Advisories + +This release addresses eight security advisories. See the linked GitHub Security Advisories for full +details, impact, and affected versions. + +* [GHSA-q8g9-jc4c-jp6q](https://github.com/openziti/ziti/security/advisories/GHSA-q8g9-jc4c-jp6q) (CVE pending) (High) - The controller buffered the entire body of every + inbound request before any authentication check and with no size cap, so an unauthenticated client could + exhaust controller memory, and crash it, by sending parallel large-body requests to endpoints such as + enrollment. +* [GHSA-j952-6x8x-jmj6](https://github.com/openziti/ziti/security/advisories/GHSA-j952-6x8x-jmj6) (CVE pending) (High) - The unauthenticated legacy enrollment path buffered + the request body a second time, allocating twice the memory per request and roughly halving the bandwidth + needed to drive the controller out of memory. Amplifies GHSA-q8g9-jc4c-jp6q. +* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a + router verified the dialing router's identity against the whole presented certificate chain instead of the + leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could + present another router's certificate as filler and be admitted on a link under that router's identity, + letting it intercept, inject, drop, or strand the circuits routed over that link. +* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session + token when creating a circuit via CreateCircuitV3, taking the dialing identity from a router-supplied + header instead. An attacker holding enrolled router credentials could create circuits on behalf of any + identity permitted to dial the service through that router, without that identity having authenticated, + yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API + sessions were not caught at circuit creation. +* [GHSA-4h58-w989-xgg4](https://github.com/openziti/ziti/security/advisories/GHSA-4h58-w989-xgg4) (CVE pending) (Medium) - The token-based enrollment endpoint skipped + audience and issuer validation when the request carried a `ziti-token-issuer-id` header, so an attacker + holding any unexpired JWT signed by a configured external JWT signer, even one minted for a different + audience, could enroll a new identity onto the network. +* [GHSA-6v5r-p2wr-q492](https://github.com/openziti/ziti/security/advisories/GHSA-6v5r-p2wr-q492) (CVE pending) (Medium) - The current-api-session certificates endpoint + performed an unscoped list, so any authenticated user could read the API session certificates (subject + DNs, fingerprints, and full PEM chains) of all identities, not just their own. +* [GHSA-whjr-3j94-gw3c](https://github.com/openziti/ziti/security/advisories/GHSA-whjr-3j94-gw3c) (CVE pending) (Medium) - A JWKS endpoint URL configured on an external JWT + signer was fetched server-side with no timeout, private-range blocking, or allowlist, letting a caller with + external-jwt-signer management access make the controller issue requests to arbitrary internal URLs, + including cloud metadata endpoints (SSRF). +* [GHSA-354c-gpg9-j988](https://github.com/openziti/ziti/security/advisories/GHSA-354c-gpg9-j988) (CVE pending) (Low) - With promptOnWake or promptOnUnlock enabled on an MFA + posture check, the edge router dereferenced a nil wake/unlock timestamp while locally evaluating an + authorized client's dial or bind, panicking and crashing the router (data-plane denial of service). + +## Contributors + +Thanks to the community members who contributed to this release. + +* [@msbusk](https://github.com/msbusk) diagnosed the circuit leak in + [#4184](https://github.com/openziti/ziti/issues/4184) and validated the fix against a + production workload. + +## Component Updates and Bug Fixes + +* github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) + * [Issue #4269](https://github.com/openziti/ziti/issues/4269) - [Backport-2.0] Router leaks LinkSendBuffer goroutines in `drainDeadlines()` — circuits accumulate until the router OOMs + * [Issue #4242](https://github.com/openziti/ziti/issues/4242) - [Backport-2.0] Legacy v1 create-circuit handler crashes the controller on JWT-prefixed tokens + * [Issue #4236](https://github.com/openziti/ziti/issues/4236) - [Backport-2.0] Ensure terminator operations are scoped by source router + * [Issue #4207](https://github.com/openziti/ziti/issues/4207) - [Backport-2.0] Lock order inversion in ConnectionTracker deadlocks the controller + * [Issue #4203](https://github.com/openziti/ziti/issues/4203) - [Backport-2.0] Controller cluster bootstrapping fixes + * [Issue #4166](https://github.com/openziti/ziti/issues/4166) - [Backport-2.0] Fabric terminator remove handlers don't verify the terminator belongs to the requesting router + * [Issue #4161](https://github.com/openziti/ziti/issues/4161) - [Backport-2.0] Leaderless controller strands terminator operations during cluster membership changes + * [Issue #4146](https://github.com/openziti/ziti/issues/4146) - [Backport-2.0] Upgraded controller rejects legacy clients' existing sessions and gives no recovery signal for invalid service tokens + * [Issue #4142](https://github.com/openziti/ziti/issues/4142) - [Backport-2.0] Service-policy enforcer deletes valid legacy sessions; type= queries use numeric id against the string-mapped symbol + * [Issue #4139](https://github.com/openziti/ziti/issues/4139) - [Backport-2.0] Add l2 service configuration types + * [Issue #4126](https://github.com/openziti/ziti/issues/4126) - [Backport-2.0] Legacy create-session signs service JWT with a mismatched session id after dedup + * [Issue #4063](https://github.com/openziti/ziti/issues/4063) - External JWT enrollment fails when a configured role attributes claims selector is absent from the JWT + * [Issue #4052](https://github.com/openziti/ziti/issues/4052) - [Backport-2.0] The `ziti` CLI now refreshes an expired access token using the cached refresh token + +# Release 2.0.2 + +## What's New + +* Security fixes (see Security Advisories below) +* Bug fixes + +## Security Advisories + +This release addresses two control-plane certificate and identity validation vulnerabilities. See the linked +GitHub Security Advisories for full details, impact, and affected versions. + +* [GHSA-mrpr-756c-xm47](https://github.com/openziti/ziti/security/advisories/GHSA-mrpr-756c-xm47) (CVE pending) (Critical) - Improper peer certificate validation on the controller + cluster mesh, router links, and metrics endpoint. TLS peer checks accepted a connection when any presented + certificate chained to the trusted CA while taking the peer identity from the leaf certificate, allowing a + peer to be admitted under a forged identity without possessing a trusted key. On HA/clustered controllers + this allows joining the controller cluster as an arbitrary controller. +* [GHSA-cc5m-7mhm-xh9f](https://github.com/openziti/ziti/security/advisories/GHSA-cc5m-7mhm-xh9f) (CVE pending) (Medium) - Control-channel connections carrying a channel-type header + bypassed router certificate and identity verification, allowing an attacker that can reach the controller + control port to be admitted as an arbitrary router identity and manipulate that router's fabric terminators, + faults, and circuit routing. Impact is limited to router data model metadata (service and identity names) and + control-plane manipulation; it does not by itself grant access to the services the network protects. + +## Component Updates and Bug Fixes + +* github.com/openziti/ziti/v2: [v2.0.1 -> v2.0.2](https://github.com/openziti/ziti/compare/v2.0.1...v2.0.2) * [Issue #4136](https://github.com/openziti/ziti/issues/4136) - [Backport-2.0] ziti tunnel ignores --dnsSvcIpRange * [Issue #4149](https://github.com/openziti/ziti/issues/4149) - [Backport-2.0] Upgrading a running 1.x controller/router to 2.x fails to create the service user - * [Issue #3891](https://github.com/openziti/ziti/issues/3891) - [Backport-2.0] OIDC auth fails when the controller's server certificate has a wildcard SAN. A wildcard SAN is now expanded to the exact hostnames listed in the new `edge-oidc` `allowedHostnames` option, which become valid OIDC issuers + * [Issue #4108](https://github.com/openziti/ziti/issues/4108) - Fix controller panic / potential data corruption by copying terminator peer data, instance secret, and eventual event data out of bolt-managed memory + # Release 2.0.1 diff --git a/common/build/flags.go b/common/build/flags.go new file mode 100644 index 000000000..dc91cb596 --- /dev/null +++ b/common/build/flags.go @@ -0,0 +1,63 @@ +package build + +import ( + "regexp" + "strings" +) + +const ( + // maxBuildFlags is the greatest number of flag names accepted from buildFlags. + maxBuildFlags = 32 + + // maxBuildFlagNameLength is the greatest length accepted for a single flag name. + maxBuildFlagNameLength = 64 +) + +// buildFlagNamePattern matches a well formed build flag name. +var buildFlagNamePattern = regexp.MustCompile(`^[A-Z0-9_]+$`) + +// buildFlags is a comma separated list of flag names supplied at build time by the linker, empty +// in a stock build: +// +// -X github.com/openziti/ziti/v2/common/build.buildFlags=ALPHA,BRAVO +// +// The symbol path is a contract with downstream builds. The linker silently ignores -X against a +// symbol it cannot resolve, so renaming this variable or moving it to another package produces a +// binary with no build flags rather than a build error. A second -X against this symbol replaces +// the first rather than adding to it: one build owner composes the whole list. +var buildFlags string + +// GetBuildFlags returns the well formed flag names supplied at build time, in the order they were +// given. It returns an empty slice for a stock build. +func GetBuildFlags() []string { + return parseBuildFlags(buildFlags) +} + +// parseBuildFlags splits raw on commas and returns the well formed names in first seen order, +// dropping blanks, duplicates, and anything outside [A-Z0-9_]. The result is never nil, so it +// serializes as an empty JSON array rather than null. The count of names and the length of each +// are capped, because the result is served over an unauthenticated API. +func parseBuildFlags(raw string) []string { + result := []string{} + seen := map[string]struct{}{} + + for _, token := range strings.Split(raw, ",") { + if len(result) == maxBuildFlags { + break + } + + name := strings.TrimSpace(token) + if len(name) > maxBuildFlagNameLength || !buildFlagNamePattern.MatchString(name) { + continue + } + + if _, ok := seen[name]; ok { + continue + } + + seen[name] = struct{}{} + result = append(result, name) + } + + return result +} diff --git a/common/build/flags_test.go b/common/build/flags_test.go new file mode 100644 index 000000000..0645dc027 --- /dev/null +++ b/common/build/flags_test.go @@ -0,0 +1,87 @@ +package build + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseBuildFlags(t *testing.T) { + tests := []struct { + name string + raw string + expected []string + }{ + { + name: "empty string yields an empty, non nil slice", + raw: "", + expected: []string{}, + }, + { + name: "single name", + raw: "ALPHA", + expected: []string{"ALPHA"}, + }, + { + name: "multiple names keep their order", + raw: "ALPHA,BRAVO,CHARLIE", + expected: []string{"ALPHA", "BRAVO", "CHARLIE"}, + }, + { + name: "surrounding whitespace is trimmed", + raw: " ALPHA , BRAVO\t,\nCHARLIE ", + expected: []string{"ALPHA", "BRAVO", "CHARLIE"}, + }, + { + name: "blank tokens are dropped", + raw: ",ALPHA,, ,BRAVO,", + expected: []string{"ALPHA", "BRAVO"}, + }, + { + name: "duplicates are dropped, first occurrence wins", + raw: "ALPHA,BRAVO,ALPHA", + expected: []string{"ALPHA", "BRAVO"}, + }, + { + name: "digits and underscores are accepted", + raw: "ALPHA_2,BRAVO_MODE,3", + expected: []string{"ALPHA_2", "BRAVO_MODE", "3"}, + }, + { + name: "malformed tokens are dropped, well formed ones survive", + raw: "alpha,BRAVO,Char-lie,DELTA ECHO,FOXTROT!,GOLF", + expected: []string{"BRAVO", "GOLF"}, + }, + { + name: "names longer than the cap are dropped", + raw: "ALPHA," + strings.Repeat("B", maxBuildFlagNameLength+1) + ",CHARLIE", + expected: []string{"ALPHA", "CHARLIE"}, + }, + { + name: "names exactly at the length cap are kept", + raw: strings.Repeat("B", maxBuildFlagNameLength), + expected: []string{strings.Repeat("B", maxBuildFlagNameLength)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := parseBuildFlags(test.raw) + + require.Equal(t, test.expected, result) + }) + } +} + +func TestParseBuildFlagsStopsAtTheCountCap(t *testing.T) { + names := make([]string, 0, maxBuildFlags+5) + for i := 0; i < maxBuildFlags+5; i++ { + names = append(names, "NAME_"+strings.Repeat("X", i)) + } + + result := parseBuildFlags(strings.Join(names, ",")) + + require.Len(t, result, maxBuildFlags, "accepted names must be capped") + require.Equal(t, names[:maxBuildFlags], result, "the first names up to the cap are the ones kept") +} diff --git a/common/cert/verify.go b/common/cert/verify.go new file mode 100644 index 000000000..31efdc0b4 --- /dev/null +++ b/common/cert/verify.go @@ -0,0 +1,71 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cert + +import ( + "crypto/x509" + "errors" + "fmt" +) + +// VerifyLeafCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// certificate in the given pool, and returns that verified leaf. Only certs[0] is verified: it is the +// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a +// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate +// intermediates supplied by the peer, never as independent grounds for acceptance - so a peer cannot be +// admitted by presenting its own leaf alongside some other certificate that happens to chain. +// +// roots is the verifying node's full trusted-CA pool (identity.CA()). Every certificate in it is a valid +// chain terminus, whether a self-signed root or an intermediate distributed as a trust anchor, matching +// how the node's TLS configuration establishes trust. No extended-key-usage restriction is applied: +// certificates issued by an external PKI with arbitrary or absent EKUs are accepted as long as the leaf +// chains to a trusted CA. +// +// additionalRoots are trust anchors the caller trusts for this check but which are not in the node's TLS +// pool, such as a CA the node itself issues peer certificates from. They are added to a copy of roots, so +// the caller's pool - which the identity shares with its live tls.Configs - is never modified. +func VerifyLeafCertChain(roots *x509.CertPool, certs []*x509.Certificate, additionalRoots ...*x509.Certificate) (*x509.Certificate, error) { + if roots == nil { + return nil, errors.New("no ca pool provided") + } + + if len(certs) == 0 { + return nil, errors.New("no certificates presented") + } + + if len(additionalRoots) > 0 { + roots = roots.Clone() + for _, additionalRoot := range additionalRoots { + roots.AddCert(additionalRoot) + } + } + + intermediates := x509.NewCertPool() + for _, intermediate := range certs[1:] { + intermediates.AddCert(intermediate) + } + + if _, err := certs[0].Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }); err != nil { + return nil, fmt.Errorf("leaf certificate not trusted: %w", err) + } + + return certs[0], nil +} diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go new file mode 100644 index 000000000..91f789eef --- /dev/null +++ b/common/cert/verify_test.go @@ -0,0 +1,254 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cert + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const vTestTrustDomain = "spiffe://verify-test" + +type vCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var vSerial int64 + +func vNextSerial() *big.Int { + vSerial++ + return big.NewInt(vSerial) +} + +func vMkCA(t *testing.T, cn string, parent *vCertAndKey) *vCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: vNextSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + signParent, signKey := tmpl, key + if parent != nil { + signParent, signKey = parent.cert, parent.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &vCertAndKey{cert: c, key: key} +} + +// vMkLeaf builds an end-entity cert. signer==nil yields a leaf that does not chain to any CA. A nil +// ekus produces a cert with no ExtKeyUsage extension (unrestricted). +func vMkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *vCertAndKey) *vCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: vNextSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: ekus, + } + if spiffePath != "" { + u, err := url.Parse(vTestTrustDomain + spiffePath) + require.NoError(t, err) + tmpl.URIs = []*url.URL{u} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &vCertAndKey{cert: c, key: key} +} + +func vPoolOf(certs ...*x509.Certificate) *x509.CertPool { + pool := x509.NewCertPool() + for _, c := range certs { + pool.AddCert(c) + } + return pool +} + +// TestVerifyLeafCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to a +// trusted CA is rejected even when another presented certificate does chain - i.e. verification is bound +// to the leaf, not to "some presented certificate verifies". +func TestVerifyLeafCertChain_RejectsUnchainedLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + roots := vPoolOf(root.cert, inter.cert) + + // A trust-anchored cert (chains to the pool) presented as an extra certificate. + extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + // The leaf itself is self-signed and does NOT chain to the pool. + unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{unchained.cert, extra.cert}) + req.Error(err, "leaf not chaining to the pool must be rejected even alongside a chaining extra cert") +} + +func TestVerifyLeafCertChain_AcceptsLegitLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + roots := vPoolOf(root.cert, inter.cert) + + legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + leaf, err := VerifyLeafCertChain(roots, []*x509.Certificate{legit.cert}) + req.NoError(err) + req.Equal(legit.cert, leaf, "returns the verified leaf (certs[0]) for identity use") +} + +func TestVerifyLeafCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + + // Pool holds ONLY the root; the intermediate must be supplied on the wire (certs[1:]). + rootOnly := vPoolOf(root.cert) + _, err := VerifyLeafCertChain(rootOnly, []*x509.Certificate{legit.cert}) + req.Error(err, "without the intermediate anywhere the leaf cannot be verified") + _, err = VerifyLeafCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) + req.NoError(err, "peer-supplied intermediate lets a valid peer verify") +} + +// TestVerifyLeafCertChain_MultipleRoots covers a trust bundle with more than one self-signed root (as a +// quickstart deployment produces, concatenating a controller root and a signer root). A leaf chaining to +// either root must verify. +func TestVerifyLeafCertChain_MultipleRoots(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signerRoot := vMkCA(t, "signer-root", nil) + signerInter := vMkCA(t, "signer-int", signerRoot) + roots := vPoolOf(ctrlRoot.cert, signerRoot.cert) + + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signerInter) + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signerInter.cert}) + req.NoError(err, "a leaf chaining to one of several trusted roots must verify") +} + +// TestVerifyLeafCertChain_IntermediateAsTrustAnchor covers a self-managed PKI that distributes a +// (non-self-signed) intermediate as the trust anchor without its root. Every certificate in the pool is +// a valid chain terminus, so a leaf chaining directly to that intermediate must verify. +func TestVerifyLeafCertChain_IntermediateAsTrustAnchor(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + leaf := vMkLeaf(t, "leaf", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + + interAnchored := vPoolOf(inter.cert) // only the intermediate distributed, no self-signed root + _, err := VerifyLeafCertChain(interAnchored, []*x509.Certificate{leaf.cert}) + req.NoError(err, "an intermediate distributed as a trust anchor must be accepted as a chain terminus") +} + +// TestVerifyLeafCertChain_ArbitraryEKU covers external PKIs whose certificates carry arbitrary or absent +// extended key usages. No EKU restriction is applied, so all of them verify as long as the chain is +// valid. +func TestVerifyLeafCertChain_ArbitraryEKU(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + roots := vPoolOf(root.cert, inter.cert) + + for _, ekus := range [][]x509.ExtKeyUsage{ + nil, + {x509.ExtKeyUsageClientAuth}, + {x509.ExtKeyUsageServerAuth}, + {x509.ExtKeyUsageEmailProtection}, + } { + leaf := vMkLeaf(t, "leaf", "/identity/real", ekus, inter) + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}) + req.NoError(err, "leaf with EKU %v must be accepted", ekus) + } +} + +func TestVerifyLeafCertChain_EmptyInputs(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + roots := vPoolOf(root.cert) + + _, err := VerifyLeafCertChain(nil, []*x509.Certificate{root.cert}) + req.Error(err, "nil pool rejected") + _, err = VerifyLeafCertChain(roots, nil) + req.Error(err, "no certs rejected") +} + +// TestVerifyLeafCertChain_AdditionalRoots covers a controller whose edge signing CA sits outside its own +// trust bundle: a router presents an enrollment certificate issued by that CA, so the leaf chains only +// once the signing bundle is supplied as an additional anchor. +func TestVerifyLeafCertChain_AdditionalRoots(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signingRoot := vMkCA(t, "signing-root", nil) + signingInter := vMkCA(t, "signing-int", signingRoot) + roots := vPoolOf(ctrlRoot.cert) + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signingInter) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signingInter.cert}) + req.Error(err, "without the signing bundle the leaf chains to nothing trusted") + + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signingInter.cert}, signingRoot.cert) + req.NoError(err, "the signing root supplied as an additional anchor lets the leaf verify") + + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}, signingInter.cert) + req.NoError(err, "an intermediate from the signing bundle is a terminus like any other anchor") + + ctrlLeaf := vMkLeaf(t, "ctrl-signed", "/identity/r2", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, ctrlRoot) + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{ctrlLeaf.cert}, signingRoot.cert) + req.NoError(err, "additional anchors do not displace the caller's own pool") +} + +// TestVerifyLeafCertChain_AdditionalRootsDoNotMutateCallerPool guards the caller's pool, which an identity +// shares with the live tls.Configs it has handed out: anchors passed for one check must not become +// permanently trusted. +func TestVerifyLeafCertChain_AdditionalRootsDoNotMutateCallerPool(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signingRoot := vMkCA(t, "signing-root", nil) + roots := vPoolOf(ctrlRoot.cert) + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signingRoot) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}, signingRoot.cert) + req.NoError(err) + + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}) + req.Error(err, "the signing root must not have been added to the caller's pool") +} diff --git a/common/concurrency/striped_id_locker.go b/common/concurrency/striped_id_locker.go new file mode 100644 index 000000000..1bcf52e4e --- /dev/null +++ b/common/concurrency/striped_id_locker.go @@ -0,0 +1,66 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package concurrency + +import "sync" + +// StripedIdLocker is a fixed-size set of mutexes selected by hashing a string +// id. Operations on the same id always serialize; operations on different ids +// proceed concurrently unless they happen to hash to the same slot. Ids that +// collide on a slot share a lock (false contention), so size the locker +// comfortably above the expected concurrency to keep collisions rare. +// +// It is intended as a lighter-weight alternative to a single coarse mutex when +// the protected work is keyed by id and most concurrent callers touch +// different ids. +type StripedIdLocker struct { + locks []sync.Mutex +} + +// NewStripedIdLocker returns a StripedIdLocker with the given number of slots. +// slots is clamped to a minimum of 1. +func NewStripedIdLocker(slots int) *StripedIdLocker { + if slots < 1 { + slots = 1 + } + return &StripedIdLocker{locks: make([]sync.Mutex, slots)} +} + +// LockFor locks the slot for id and returns a function that unlocks it. The +// returned function must be called exactly once. Typical use: +// +// defer locker.LockFor(id)() +func (self *StripedIdLocker) LockFor(id string) func() { + m := &self.locks[self.indexFor(id)] + m.Lock() + return m.Unlock +} + +// indexFor maps id to a slot using FNV-1a (inlined to avoid allocating a hasher +// on this hot path). +func (self *StripedIdLocker) indexFor(id string) uint32 { + const ( + offset32 = 2166136261 + prime32 = 16777619 + ) + h := uint32(offset32) + for i := 0; i < len(id); i++ { + h ^= uint32(id[i]) + h *= prime32 + } + return h % uint32(len(self.locks)) +} diff --git a/common/concurrency/striped_id_locker_test.go b/common/concurrency/striped_id_locker_test.go new file mode 100644 index 000000000..4c578b386 --- /dev/null +++ b/common/concurrency/striped_id_locker_test.go @@ -0,0 +1,127 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package concurrency + +import ( + "fmt" + "sync" + "testing" + "time" +) + +// TestStripedIdLocker_SameIdSerializes verifies that two lockers for the same id +// are mutually exclusive: the second LockFor blocks until the first unlocks. +func TestStripedIdLocker_SameIdSerializes(t *testing.T) { + locker := NewStripedIdLocker(16) + + unlock := locker.LockFor("link-1") + + acquired := make(chan struct{}) + go func() { + secondUnlock := locker.LockFor("link-1") + close(acquired) + secondUnlock() + }() + + select { + case <-acquired: + t.Fatal("second LockFor for the same id acquired while the first was still held") + case <-time.After(50 * time.Millisecond): + // expected: still blocked + } + + unlock() + + select { + case <-acquired: + // expected: unblocked once the first lock was released + case <-time.After(time.Second): + t.Fatal("second LockFor did not acquire after the first was released") + } +} + +// TestStripedIdLocker_MutualExclusion runs many goroutines incrementing per-id +// counters guarded only by the striped locker. With correct per-id +// serialization every counter ends at the expected total. Run with -race to +// also catch incorrect sharing. +func TestStripedIdLocker_MutualExclusion(t *testing.T) { + const ( + ids = 50 + goroutines = 32 + incsPerWorker = 200 + ) + + // Use more ids than slots so multiple ids share slots; correctness must not + // depend on a 1:1 id-to-slot mapping. + locker := NewStripedIdLocker(8) + counters := make([]int, ids) + + var wg sync.WaitGroup + for g := 0; g < goroutines; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < incsPerWorker; i++ { + id := fmt.Sprintf("id-%d", i%ids) + unlock := locker.LockFor(id) + counters[i%ids]++ + unlock() + } + }() + } + wg.Wait() + + expected := goroutines * incsPerWorker / ids + for i, c := range counters { + if c != expected { + t.Fatalf("counter[%d] = %d, expected %d", i, c, expected) + } + } +} + +// TestStripedIdLocker_DifferentIdsConcurrent verifies that ids landing on +// different slots do not block each other. +func TestStripedIdLocker_DifferentIdsConcurrent(t *testing.T) { + locker := NewStripedIdLocker(256) + + // Find two ids that map to different slots. + a := "router-a" + var b string + for i := 0; ; i++ { + candidate := fmt.Sprintf("router-%d", i) + if locker.indexFor(candidate) != locker.indexFor(a) { + b = candidate + break + } + } + + unlockA := locker.LockFor(a) + defer unlockA() + + done := make(chan struct{}) + go func() { + locker.LockFor(b)() // should not block on a's slot + close(done) + }() + + select { + case <-done: + // expected + case <-time.After(time.Second): + t.Fatal("LockFor on a different slot blocked while an unrelated slot was held") + } +} diff --git a/common/config/heartbeats.go b/common/config/heartbeats.go new file mode 100644 index 000000000..b7aa6d27b --- /dev/null +++ b/common/config/heartbeats.go @@ -0,0 +1,63 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package config + +import ( + "fmt" + + "github.com/openziti/channel/v4" +) + +// ValidateHeartbeatOptions reports whether options describe a cadence that can keep a channel up. +// Callers pass loaded configuration, so the returned error names the configuration keys and is +// suitable for returning straight out of config loading. +// +// Apply this wherever the heartbeat callback closes the channel after CloseUnresponsiveTimeout +// without a response. A callback that only records latency has no deadline to sample and does not +// need it. A zero CloseUnresponsiveTimeout disables the teardown, and only the check interval is +// constrained. +func ValidateHeartbeatOptions(options *channel.HeartbeatOptions) error { + if options == nil { + return nil + } + + // ConfigureHeartbeat hands this straight to time.NewTicker on a bare goroutine, which panics on + // a non-positive interval and takes the process with it. + if options.CheckInterval <= 0 { + return fmt.Errorf("heartbeat checkInterval (%v) must be greater than zero", options.CheckInterval) + } + + if options.CloseUnresponsiveTimeout <= 0 { + return nil + } + + // The check interval is the sampling rate for the timeout, and the response time it reads can be + // a full interval stale, so at or above the timeout it condemns channels that are answering. + if options.CheckInterval >= options.CloseUnresponsiveTimeout { + return fmt.Errorf("heartbeat checkInterval (%v) must be less than closeUnresponsiveTimeout (%v), "+ + "otherwise the check reads a stale response time and closes healthy channels", + options.CheckInterval, options.CloseUnresponsiveTimeout) + } + + if options.SendInterval >= options.CloseUnresponsiveTimeout { + return fmt.Errorf("heartbeat sendInterval (%v) must be less than closeUnresponsiveTimeout (%v), "+ + "otherwise a channel is closed between scheduled heartbeats", + options.SendInterval, options.CloseUnresponsiveTimeout) + } + + return nil +} diff --git a/common/config/heartbeats_test.go b/common/config/heartbeats_test.go new file mode 100644 index 000000000..4aa09a64d --- /dev/null +++ b/common/config/heartbeats_test.go @@ -0,0 +1,107 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package config + +import ( + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/stretchr/testify/require" +) + +func TestValidateHeartbeatOptions(t *testing.T) { + options := func(send, check, closeTimeout time.Duration) *channel.HeartbeatOptions { + return &channel.HeartbeatOptions{ + SendInterval: send, + CheckInterval: check, + CloseUnresponsiveTimeout: closeTimeout, + } + } + + tests := []struct { + name string + options *channel.HeartbeatOptions + errIs string + }{ + { + name: "the library defaults are accepted", + options: channel.DefaultHeartbeatOptions(), + }, + { + name: "nil is accepted, since an absent config is the caller's default", + options: nil, + }, + { + name: "a cadence well inside the timeout is accepted", + options: options(10*time.Second, time.Second, 30*time.Second), + }, + { + name: "a zero check interval is rejected, since the pulse ticker panics on it", + options: options(10*time.Second, 0, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a negative check interval is rejected", + options: options(10*time.Second, -time.Second, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a check interval above the timeout is rejected", + options: options(10*time.Second, time.Minute, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a check interval equal to the timeout is rejected", + options: options(10*time.Second, 30*time.Second, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a send interval above the timeout is rejected", + options: options(time.Minute, time.Second, 30*time.Second), + errIs: "sendInterval", + }, + { + name: "a send interval equal to the timeout is rejected", + options: options(30*time.Second, time.Second, 30*time.Second), + errIs: "sendInterval", + }, + { + // A zero timeout disables the teardown, so there is no deadline to outpace and only the + // ticker's own requirement is left to enforce. + name: "a disabled teardown leaves the intervals unconstrained", + options: options(time.Hour, time.Minute, 0), + }, + { + name: "a disabled teardown still rejects a zero check interval", + options: options(10*time.Second, 0, 0), + errIs: "checkInterval", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateHeartbeatOptions(test.options) + if test.errIs == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), test.errIs) + }) + } +} diff --git a/common/ctrl_msg/messages.go b/common/ctrl_msg/messages.go index 7718c9ef2..3872dbd72 100644 --- a/common/ctrl_msg/messages.go +++ b/common/ctrl_msg/messages.go @@ -207,7 +207,9 @@ func DecodeCreateCircuitV2Response(m *channel.Message) (*CreateCircuitV2Response // CreateCircuitV3Request is sent from a router to the controller to create a circuit // without a service session token. The router has already authorized the dial locally // via RDM and provides the identity and service IDs directly, along with a pre-assigned -// circuit ID. +// circuit ID. ApiSessionToken is required and must be the token of the dialing identity: +// the controller validates it and treats its claims, rather than IdentityId, as the +// authoritative identity. type CreateCircuitV3Request struct { IdentityId string ServiceId string diff --git a/common/ctrlchan/channel_test.go b/common/ctrlchan/channel_test.go index 092a9f765..c8f0d116d 100644 --- a/common/ctrlchan/channel_test.go +++ b/common/ctrlchan/channel_test.go @@ -653,3 +653,77 @@ func TestDialCtrlChannel_ReconnectCycle(t *testing.T) { }, 5*time.Second, 10*time.Millisecond, "loop iteration %d, should match channel iteration %d", i, dialChannel.iteration.Load()) } } + +// TestReject_BindErrorNotEstablishedOrRxStarted validates the boundary that router-connect rejection +// relies on: when the ctrl-channel bind handler returns an error (as CtrlAccepter.Bind does when +// network.ConnectRouter rejects a busy slot), channel.NewChannel returns that error, the MultiListener +// closes the underlay without registering the channel, and the receive loop never starts. This +// no-rx / no-register outcome is the contract the fake-CtrlChannel unit tests cannot observe. +func TestReject_BindErrorNotEstablishedOrRxStarted(t *testing.T) { + req := require.New(t) + + listenerAddr := "tcp:127.0.0.1:40010" + id := &identity.TokenId{Token: "test-controller"} + + var bindAttempts atomic.Int32 + var established atomic.Int32 + var rxFired atomic.Bool + + multiListener := channel.NewMultiListener( + func(underlay channel.Underlay, closeCallback func()) (channel.MultiChannel, error) { + bindAttempts.Add(1) + listenerChannel := NewListenerCtrlChannel() + multiConfig := &channel.MultiChannelConfig{ + LogicalName: "ctrl/" + underlay.ConnectionId(), + Options: channel.DefaultOptions(), + UnderlayHandler: listenerChannel, + Underlay: underlay, + BindHandler: channel.BindHandlerF(func(binding channel.Binding) error { + // Would flip if the rx loop ever started for this (rejected) channel. + binding.AddReceiveHandlerF(echoContentType, func(*channel.Message, channel.Channel) { + rxFired.Store(true) + }) + // Reject, as network.ConnectRouter does for a busy slot. + return fmt.Errorf("simulated router connect rejected (busy slot)") + }), + } + + multiCh, err := channel.NewMultiChannel(multiConfig) + if err != nil { + // Rejected: NewMultiChannel closed the underlay and never started rx; do not register. + return nil, err + } + established.Add(1) + return multiCh, nil + }, + func(underlay channel.Underlay) error { + return fmt.Errorf("ungrouped connections not supported") + }, + ) + + bindAddr, err := transport.ParseAddress(listenerAddr) + req.NoError(err) + listenerConfig := channel.ListenerConfig{ConnectOptions: channel.DefaultOptions().ConnectOptions} + listener, err := channel.NewClassicListenerF(id, bindAddr, listenerConfig, multiListener.AcceptUnderlay) + req.NoError(err) + defer func() { _ = listener.Close() }() + + headers := channel.Headers{} + headers.PutStringHeader(channel.TypeHeader, ChannelTypeDefault) + headers.PutBoolHeader(channel.IsGroupedHeader, true) + headers.PutBoolHeader(channel.IsFirstGroupConnection, true) + + dialer := channel.NewClassicDialer(channel.DialerConfig{Identity: id, Endpoint: bindAddr}) + underlay, err := dialer.CreateWithHeaders(5*time.Second, headers) + req.NoError(err) + defer func() { _ = underlay.Close() }() + + // The dial reaches the listener's bind (the hello is acked before the factory runs). + req.Eventually(func() bool { return bindAttempts.Load() >= 1 }, 5*time.Second, 10*time.Millisecond, + "listener bind should have run for the dialed connection") + + // The rejected connection is never established, and its receive loop never starts. Give any spurious + // rx a moment to (not) happen. + req.Never(func() bool { return established.Load() != 0 || rxFired.Load() }, 500*time.Millisecond, 50*time.Millisecond, + "a rejected bind must not establish a channel or start its receive loop") +} diff --git a/common/oidc_tokens.go b/common/oidc_tokens.go index 9714d51af..7f8e9d685 100644 --- a/common/oidc_tokens.go +++ b/common/oidc_tokens.go @@ -209,6 +209,26 @@ func (c *ServiceAccessClaims) HasAudience(targetAud string) bool { return false } +// InvalidTokenError indicates a token was rejected because of the token itself (bad signature or +// parse, expiry, unexpected claims/audience/type, a mismatched api session, or revocation) rather +// than an infrastructure failure (e.g. a datastore read) encountered while validating it. Callers +// use it to distinguish a client that should discard and re-create its session from a transient +// controller failure that must not invalidate otherwise-valid client state. +type InvalidTokenError struct { + Err error +} + +func (e *InvalidTokenError) Error() string { + if e.Err == nil { + return "invalid token" + } + return e.Err.Error() +} + +func (e *InvalidTokenError) Unwrap() error { + return e.Err +} + type AccessClaims struct { oidc.AccessTokenClaims CustomClaims diff --git a/common/pb/cmd_pb/cmd.pb.go b/common/pb/cmd_pb/cmd.pb.go index fdd5496ae..770b10477 100644 --- a/common/pb/cmd_pb/cmd.pb.go +++ b/common/pb/cmd_pb/cmd.pb.go @@ -599,6 +599,7 @@ type SyncSnapshotCommand struct { SnapshotId string `protobuf:"bytes,1,opt,name=snapshotId,proto3" json:"snapshotId,omitempty"` Snapshot []byte `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + ClusterId string `protobuf:"bytes,3,opt,name=clusterId,proto3" json:"clusterId,omitempty"` } func (x *SyncSnapshotCommand) Reset() { @@ -647,6 +648,13 @@ func (x *SyncSnapshotCommand) GetSnapshot() []byte { return nil } +func (x *SyncSnapshotCommand) GetClusterId() string { + if x != nil { + return x.ClusterId + } + return "" +} + type InitClusterIdCommand struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1413,164 +1421,166 @@ var file_cmd_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0x51, + 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0x6f, 0x0a, 0x13, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x22, 0x54, 0x0a, 0x14, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x49, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, - 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x6c, - 0x69, 0x6e, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, - 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x6b, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x49, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, - 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, - 0x03, 0x63, 0x74, 0x78, 0x22, 0x91, 0x01, 0x0a, 0x08, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x1e, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x22, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, - 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x83, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, - 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x32, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, - 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x54, 0x61, 0x67, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, - 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x4e, - 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, - 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x30, - 0x0a, 0x16, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x22, 0xa0, 0x04, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, - 0x72, 0x73, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x54, 0x72, - 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, - 0x61, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x7a, 0x69, 0x74, - 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x58, - 0x0a, 0x11, 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x7a, 0x69, 0x74, 0x69, - 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, - 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, - 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x16, 0x43, 0x74, 0x72, 0x6c, - 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, - 0x62, 0x2e, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x8b, 0x05, 0x0a, 0x0a, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, - 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, - 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, - 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, - 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x08, - 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, - 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, - 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, - 0x35, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, - 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1a, - 0x0a, 0x08, 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x61, - 0x76, 0x65, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x73, 0x61, 0x76, 0x65, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x74, - 0x72, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x43, 0x74, 0x72, 0x6c, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0xa5, 0x01, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x61, - 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x6d, 0x74, 0x75, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, - 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x2a, 0xc3, 0x01, 0x0a, 0x0b, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, - 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, - 0x65, 0x10, 0x82, 0x10, 0x12, 0x16, 0x0a, 0x11, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0x83, 0x10, 0x12, 0x18, 0x0a, 0x13, - 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x10, 0x84, 0x10, 0x12, 0x17, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x85, 0x10, 0x12, - 0x1a, 0x0a, 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x86, 0x10, 0x12, 0x22, 0x0a, 0x1d, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x68, 0x69, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x87, 0x10, 0x2a, - 0x9e, 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x01, 0x12, - 0x14, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, - 0x79, 0x70, 0x65, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x53, - 0x79, 0x6e, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10, 0x0a, 0x12, 0x11, 0x0a, - 0x0d, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x10, 0x0b, - 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, - 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x2f, 0x70, - 0x62, 0x2f, 0x63, 0x6d, 0x64, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x22, + 0x54, 0x0a, 0x14, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, + 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, + 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x6b, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, + 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x49, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x49, 0x64, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, + 0x74, 0x78, 0x22, 0x91, 0x01, 0x0a, 0x08, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1e, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x22, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1c, 0x0a, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x48, 0x00, 0x52, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x83, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x32, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, + 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, + 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0b, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x4e, 0x0a, 0x09, + 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, 0x69, 0x74, + 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x30, 0x0a, 0x16, + 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0xa0, + 0x04, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, + 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, + 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, + 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x31, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, + 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, + 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x11, + 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, + 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x74, 0x72, + 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x11, 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, + 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x16, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, + 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x23, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0x8b, 0x05, 0x0a, 0x0a, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1e, + 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, + 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, + 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, + 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x08, 0x70, 0x65, + 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x7a, + 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, + 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, + 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x7a, 0x69, + 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x61, 0x76, 0x65, + 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0f, 0x73, 0x61, 0x76, 0x65, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x74, 0x72, 0x6c, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x74, + 0x72, 0x6c, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0xa5, 0x01, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x61, 0x72, 0x64, + 0x77, 0x61, 0x72, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6d, + 0x74, 0x75, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, 0x14, 0x0a, + 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x2a, 0xc3, 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x0f, + 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, + 0x82, 0x10, 0x12, 0x16, 0x0a, 0x11, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0x83, 0x10, 0x12, 0x18, 0x0a, 0x13, 0x53, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x10, 0x84, 0x10, 0x12, 0x17, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x85, 0x10, 0x12, 0x1a, 0x0a, + 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x86, 0x10, 0x12, 0x22, 0x0a, 0x1d, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x87, 0x10, 0x2a, 0x9e, 0x01, + 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, + 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x01, 0x12, 0x14, 0x0a, + 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, + 0x65, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x79, 0x6e, + 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10, 0x0a, 0x12, 0x11, 0x0a, 0x0d, 0x49, + 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x10, 0x0b, 0x42, 0x26, + 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, + 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x2f, + 0x63, 0x6d, 0x64, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/common/pb/cmd_pb/cmd.proto b/common/pb/cmd_pb/cmd.proto index 070d36c37..26ba2f026 100644 --- a/common/pb/cmd_pb/cmd.proto +++ b/common/pb/cmd_pb/cmd.proto @@ -75,6 +75,7 @@ message DeleteEntityCommand { message SyncSnapshotCommand { string snapshotId = 1; bytes snapshot = 2; + string clusterId = 3; } message InitClusterIdCommand { diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go index 07ceff7d4..94fd9c9cb 100644 --- a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go +++ b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.21.12 +// protoc v4.23.4 // source: edge_ctrl.proto package edge_ctrl_pb @@ -627,8 +627,11 @@ func (DataState_Action) EnumDescriptor() ([]byte, []int) { type DataState_PublicKey_Usage int32 const ( - DataState_PublicKey_JWTValidation DataState_PublicKey_Usage = 0 - DataState_PublicKey_ClientX509CertValidation DataState_PublicKey_Usage = 1 + DataState_PublicKey_JWTValidation DataState_PublicKey_Usage = 0 + // Deprecated: Marked as deprecated in edge_ctrl.proto. + DataState_PublicKey_ClientX509CertValidation DataState_PublicKey_Usage = 1 //superseded by FirstPartyX509CertValidation and ThirdPartyX509CertValidation + DataState_PublicKey_FirstPartyX509CertValidation DataState_PublicKey_Usage = 2 //network-internal CAs: controller certs, edge signing CA roots + DataState_PublicKey_ThirdPartyX509CertValidation DataState_PublicKey_Usage = 3 //externally registered CAs ) // Enum value maps for DataState_PublicKey_Usage. @@ -636,10 +639,14 @@ var ( DataState_PublicKey_Usage_name = map[int32]string{ 0: "JWTValidation", 1: "ClientX509CertValidation", + 2: "FirstPartyX509CertValidation", + 3: "ThirdPartyX509CertValidation", } DataState_PublicKey_Usage_value = map[string]int32{ - "JWTValidation": 0, - "ClientX509CertValidation": 1, + "JWTValidation": 0, + "ClientX509CertValidation": 1, + "FirstPartyX509CertValidation": 2, + "ThirdPartyX509CertValidation": 3, } ) @@ -4683,6 +4690,7 @@ type DataState_PublicKey struct { Kid string `protobuf:"bytes,2,opt,name=kid,proto3" json:"kid,omitempty"` //key id/fingerprint Usages []DataState_PublicKey_Usage `protobuf:"varint,3,rep,packed,name=usages,proto3,enum=ziti.edge_ctrl.pb.DataState_PublicKey_Usage" json:"usages,omitempty"` // what the public key in data is used for Format DataState_PublicKey_Format `protobuf:"varint,4,opt,name=format,proto3,enum=ziti.edge_ctrl.pb.DataState_PublicKey_Format" json:"format,omitempty"` //the format of the public key in data and chain + Intermediates [][]byte `protobuf:"bytes,5,rep,name=intermediates,proto3" json:"intermediates,omitempty"` //intermediate CA certs chaining to the anchor in data, same format unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4745,6 +4753,13 @@ func (x *DataState_PublicKey) GetFormat() DataState_PublicKey_Format { return DataState_PublicKey_X509CertDer } +func (x *DataState_PublicKey) GetIntermediates() [][]byte { + if x != nil { + return x.Intermediates + } + return nil +} + type DataState_PostureCheck struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -5451,7 +5466,7 @@ const file_edge_ctrl_proto_rawDesc = "" + "\x04data\x18\x01 \x03(\v2\".ziti.edge_ctrl.pb.Cache.DataEntryR\x04data\x1a7\n" + "\tDataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xb5$\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xa4%\n" + "\tDataState\x12:\n" + "\x06events\x18\x01 \x03(\v2\".ziti.edge_ctrl.pb.DataState.EventR\x06events\x12\x1a\n" + "\bendIndex\x18\x02 \x01(\x04R\bendIndex\x12\x1e\n" + @@ -5540,15 +5555,18 @@ const file_edge_ctrl_proto_rawDesc = "" + "configType\x18\x11 \x01(\v2'.ziti.edge_ctrl.pb.DataState.ConfigTypeH\x00R\n" + "configType\x12=\n" + "\x06config\x18\x12 \x01(\v2#.ziti.edge_ctrl.pb.DataState.ConfigH\x00R\x06configB\a\n" + - "\x05Model\x1a\xa6\x02\n" + + "\x05Model\x1a\x95\x03\n" + "\tPublicKey\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data\x12\x10\n" + "\x03kid\x18\x02 \x01(\tR\x03kid\x12D\n" + "\x06usages\x18\x03 \x03(\x0e2,.ziti.edge_ctrl.pb.DataState.PublicKey.UsageR\x06usages\x12E\n" + - "\x06format\x18\x04 \x01(\x0e2-.ziti.edge_ctrl.pb.DataState.PublicKey.FormatR\x06format\"8\n" + + "\x06format\x18\x04 \x01(\x0e2-.ziti.edge_ctrl.pb.DataState.PublicKey.FormatR\x06format\x12$\n" + + "\rintermediates\x18\x05 \x03(\fR\rintermediates\"\x80\x01\n" + "\x05Usage\x12\x11\n" + - "\rJWTValidation\x10\x00\x12\x1c\n" + - "\x18ClientX509CertValidation\x10\x01\",\n" + + "\rJWTValidation\x10\x00\x12 \n" + + "\x18ClientX509CertValidation\x10\x01\x1a\x02\b\x01\x12 \n" + + "\x1cFirstPartyX509CertValidation\x10\x02\x12 \n" + + "\x1cThirdPartyX509CertValidation\x10\x03\",\n" + "\x06Format\x12\x0f\n" + "\vX509CertDer\x10\x00\x12\x11\n" + "\rPKIXPublicKey\x10\x01\x1a\xa3\t\n" + diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.proto b/common/pb/edge_ctrl_pb/edge_ctrl.proto index d42c643bf..e5247f3d7 100644 --- a/common/pb/edge_ctrl_pb/edge_ctrl.proto +++ b/common/pb/edge_ctrl_pb/edge_ctrl.proto @@ -258,10 +258,13 @@ message DataState { string kid = 2; //key id/fingerprint repeated Usage usages = 3; // what the public key in data is used for Format format = 4; //the format of the public key in data and chain + repeated bytes intermediates = 5; //intermediate CA certs chaining to the anchor in data, same format enum Usage { JWTValidation = 0; - ClientX509CertValidation = 1; + ClientX509CertValidation = 1 [deprecated = true]; //superseded by FirstPartyX509CertValidation and ThirdPartyX509CertValidation + FirstPartyX509CertValidation = 2; //network-internal CAs: controller certs, edge signing CA roots + ThirdPartyX509CertValidation = 3; //externally registered CAs } enum Format { diff --git a/common/public_key_validate_roundtrip_test.go b/common/public_key_validate_roundtrip_test.go new file mode 100644 index 000000000..da13e339b --- /dev/null +++ b/common/public_key_validate_roundtrip_test.go @@ -0,0 +1,59 @@ +package common + +import ( + "testing" + + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// Test_PublicKeyIntermediatesSurviveValidateRoundTrip mirrors the router-data-model validate +// flow: a router's live model and a bare model rebuilt from a proto-round-tripped controller +// snapshot must agree on a public key that carries intermediates. +func Test_PublicKeyIntermediatesSurviveValidateRoundTrip(t *testing.T) { + req := require.New(t) + + key := &edge_ctrl_pb.DataState_PublicKey{ + Kid: "kid1", + Data: []byte("anchor-der"), + Format: edge_ctrl_pb.DataState_PublicKey_X509CertDer, + Usages: []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + }, + Intermediates: [][]byte{[]byte("intermediate-der")}, + } + evt := &edge_ctrl_pb.DataState_Event{ + Action: edge_ctrl_pb.DataState_Create, + Model: &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: key}, + IsSynthetic: true, + } + + current := NewBareRouterDataModel() + current.WhileLocked(func(u uint64) { + current.Handle(1, evt) + current.SetCurrentIndex(1) + }) + + state := &edge_ctrl_pb.DataState{Events: []*edge_ctrl_pb.DataState_Event{evt}, EndIndex: 1} + request := &edge_ctrl_pb.RouterDataModelValidateRequest{State: state} + wire, err := proto.Marshal(request) + req.NoError(err) + parsed := &edge_ctrl_pb.RouterDataModelValidateRequest{} + req.NoError(proto.Unmarshal(wire, parsed)) + + model := NewBareRouterDataModel() + model.WhileLocked(func(u uint64) { + for _, e := range parsed.State.Events { + model.Handle(parsed.State.EndIndex, e) + } + model.SetCurrentIndex(parsed.State.EndIndex) + }) + + var diffs []string + current.Validate(model, func(entityType string, id string, diffType DiffType, detail string) { + diffs = append(diffs, entityType+" "+id+" "+detail) + }) + req.Empty(diffs) +} diff --git a/common/security_tokens.go b/common/security_tokens.go index 5bc55e61a..f35f63cbe 100644 --- a/common/security_tokens.go +++ b/common/security_tokens.go @@ -142,6 +142,12 @@ type TokenIssuerCache interface { // GetIssuerByKid returns the TokenIssuer that owns the given key ID GetIssuerByKid(kid string) TokenIssuer + + // GetControllerIssuerByKid returns the controller TokenIssuer that owns the given key ID, + // or nil if no controller issuer claims that kid. A controller issuer's key ID is the + // fingerprint of its TLS certificate, so this resolves controller-issued tokens by kid + // without consulting external signers. + GetControllerIssuerByKid(kid string) TokenIssuer } // SecurityToken is the result of verifying the primary security token presented on a request. @@ -505,12 +511,20 @@ func (s *SecurityTokenCtx) processHeaders() error { continue } + // Bind controller-issued tokens first by kid. A controller issuer's kid is the + // fingerprint of its TLS certificate, so it does not collide with external signers. + // This must precede the issuer-string lookup so an external signer configured with a + // controller's OIDC issuer URL cannot capture controller access tokens. kid := bearerToken.Kid() if kid != "" { - bearerToken.TokenIssuer = s.tokenIssuerCache.GetIssuerByKid(kid) + bearerToken.TokenIssuer = s.tokenIssuerCache.GetControllerIssuerByKid(kid) } + // Otherwise bind external signers by their exact issuer claim. Binding by kid is not + // used as a fallback: external signers can share a kid (shared signing-key pools), so + // kid resolution is ambiguous, and binding a token whose issuer matches no configured + // signer to an unrelated signer that happens to share the kid would be incorrect. if bearerToken.TokenIssuer == nil { issuer := bearerToken.Issuer() if issuer != "" { diff --git a/controller/api/body_limit.go b/controller/api/body_limit.go new file mode 100644 index 000000000..acbb2fc67 --- /dev/null +++ b/controller/api/body_limit.go @@ -0,0 +1,23 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package api + +// MaxRequestBodySize is the maximum number of HTTP request body bytes the controller's +// web APIs accept per request. Request bodies are buffered into memory before +// authentication, so this cap bounds the memory an unauthenticated client can consume. +// Requests with larger bodies are rejected with HTTP 413 Request Entity Too Large. +const MaxRequestBodySize = 1024 * 1024 diff --git a/controller/apierror/helpers.go b/controller/apierror/helpers.go index d1e25747f..784683a82 100644 --- a/controller/apierror/helpers.go +++ b/controller/apierror/helpers.go @@ -49,6 +49,16 @@ func NewCouldNotReadBody(err error) *errorz.ApiError { } } +// NewRequestEntityTooLarge returns the 413 ApiError raised when a request body exceeds the +// maximum size the web APIs accept. +func NewRequestEntityTooLarge() *errorz.ApiError { + return &errorz.ApiError{ + AppCode: RequestEntityTooLargeCode, + Message: RequestEntityTooLargeMessage, + Status: RequestEntityTooLargeStatus, + } +} + func NewInvalidAuth() *errorz.ApiError { return &errorz.ApiError{ AppCode: InvalidAuthCode, diff --git a/controller/apierror/messages.go b/controller/apierror/messages.go index 143592547..ec9be1eb5 100644 --- a/controller/apierror/messages.go +++ b/controller/apierror/messages.go @@ -31,6 +31,10 @@ const ( CouldNotReadBodyMessage string = "The body of the request could not be read" CouldNotReadBodyStatus int = http.StatusInternalServerError + RequestEntityTooLargeCode string = "REQUEST_ENTITY_TOO_LARGE" + RequestEntityTooLargeMessage string = "The request body exceeds the maximum accepted size" + RequestEntityTooLargeStatus int = http.StatusRequestEntityTooLarge + InvalidUuidCode string = "INVALID_UUID" InvalidUuidMessage string = "The supplied UUID is invalid" InvalidUuidStatus int = http.StatusBadRequest diff --git a/controller/command/command.go b/controller/command/command.go index cd842cfe3..01b7aeaa6 100644 --- a/controller/command/command.go +++ b/controller/command/command.go @@ -18,14 +18,15 @@ package command import ( "reflect" + "sync" "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" "github.com/openziti/foundation/v2/debugz" "github.com/openziti/foundation/v2/rate" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/ctrl_pb" "github.com/openziti/ziti/v2/controller/change" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/sirupsen/logrus" ) @@ -42,6 +43,14 @@ type Command interface { Encode() ([]byte, error) } +// CriticalCommand marks commands that establish base state (e.g. a snapshot restore). A failed apply +// of one must halt the node rather than log-and-advance the raft index, which would leave the node +// caught up on index but missing data. Ordinary commands are logged and skipped on failure. +type CriticalCommand interface { + Command + IsCriticalCommand() +} + // Validatable instances can be validated. Command instances which implement Validable will be validated // before Command.Apply is called type Validatable interface { @@ -59,12 +68,27 @@ type Dispatcher interface { GetRateLimiter() rate.RateLimiter Bootstrap() error CtrlAddresses() (uint64, []string, []*ctrl_pb.CtrlDetail) + + // GetDecoders returns the command decoder registry used to decode commands dispatched + // through this dispatcher. Each dispatcher owns its own registry so multiple controllers + // in one process don't decode each other's commands. + GetDecoders() Decoders } // LocalDispatcher should be used when running a non-clustered system type LocalDispatcher struct { EncodeDecodeCommands bool Limiter rate.RateLimiter + + decodersInit sync.Once + decoders Decoders +} + +func (self *LocalDispatcher) GetDecoders() Decoders { + self.decodersInit.Do(func() { + self.decoders = NewDecoders() + }) + return self.decoders } func (self *LocalDispatcher) Bootstrap() error { @@ -117,7 +141,7 @@ func (self *LocalDispatcher) Dispatch(command Command) error { if err != nil { return err } - cmd, err := GetDefaultDecoders().Decode(bytes) + cmd, err := self.GetDecoders().Decode(bytes) if err != nil { return err } diff --git a/controller/command/generic_cmds.go b/controller/command/generic_cmds.go index e2d31b14e..8b29d0e42 100644 --- a/controller/command/generic_cmds.go +++ b/controller/command/generic_cmds.go @@ -1,11 +1,11 @@ package command import ( - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/cmd_pb" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/fields" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/pkg/errors" ) @@ -144,9 +144,12 @@ func (self *DeleteEntityCommand) GetChangeContext() *change.Context { return self.Context } +var _ CriticalCommand = (*SyncSnapshotCommand)(nil) + type SyncSnapshotCommand struct { TimelineId string Snapshot []byte + ClusterId string SnapshotSink func(cmd *SyncSnapshotCommand, index uint64) error } @@ -155,10 +158,14 @@ func (self *SyncSnapshotCommand) Apply(ctx boltz.MutateContext) error { return self.SnapshotSink(self, changeCtx.RaftIndex) } +// IsCriticalCommand marks SyncSnapshotCommand as base state: a failed apply halts rather than advances. +func (self *SyncSnapshotCommand) IsCriticalCommand() {} + func (self *SyncSnapshotCommand) Encode() ([]byte, error) { return cmd_pb.EncodeProtobuf(&cmd_pb.SyncSnapshotCommand{ SnapshotId: self.TimelineId, Snapshot: self.Snapshot, + ClusterId: self.ClusterId, }) } diff --git a/controller/command/rate_limiter.go b/controller/command/rate_limiter.go index 51c7afc27..a204b73e7 100644 --- a/controller/command/rate_limiter.go +++ b/controller/command/rate_limiter.go @@ -438,6 +438,17 @@ func WasRateLimited(err error) bool { return false } +// WasLeaderless returns true if the given error indicates that a command could not be dispatched because the +// cluster currently has no leader. This is a transient condition during membership changes; callers should +// treat it like rate limiting and signal the requester to retry rather than fail permanently. +func WasLeaderless(err error) bool { + var apiErr *errorz.ApiError + if errors.As(err, &apiErr) { + return apiErr.AppCode == apierror.ClusterHasNoLeaderCode + } + return false +} + // AdaptiveRateLimitTrackerConfig contains configuration values used to create a new AdaptiveRateLimitTracker type AdaptiveRateLimitTrackerConfig struct { AdaptiveRateLimiterConfig diff --git a/controller/config/config_edge.go b/controller/config/config_edge.go index 3eae0cf13..cf9b3afd7 100644 --- a/controller/config/config_edge.go +++ b/controller/config/config_edge.go @@ -37,6 +37,7 @@ import ( "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/controller/command" "github.com/pkg/errors" + "golang.org/x/net/idna" ) const ( @@ -72,6 +73,17 @@ const ( DefaultIdentityOnlineStatusUnknownTimeout = 5 * time.Minute DefaultIdentityOnlineStatusSource = IdentityStatusSourceHybrid + + // DefaultJwksFetchBlockPrivateAddresses leaves private and loopback addresses reachable by + // default so that deployments using an internal IdP keep working. Metadata and link-local + // addresses are blocked regardless of this setting. + DefaultJwksFetchBlockPrivateAddresses = false + + // DefaultJwksFetchTimeout bounds the total time spent fetching a JWKS endpoint. + DefaultJwksFetchTimeout = 5 * time.Second + + // DefaultJwksFetchMaxRedirects bounds how many redirects a JWKS fetch will follow. + DefaultJwksFetchMaxRedirects = 5 ) type Enrollment struct { @@ -136,6 +148,67 @@ func (o *Oidc) MaxTokenDuration() time.Duration { return common.MaxTokenDuration(o.RefreshTokenDuration, o.AccessTokenDuration, o.IdTokenDuration) } +// ExternalJwtSigners holds settings that govern how the controller interacts with +// external JWT signers. +type ExternalJwtSigners struct { + JwksFetch JwksFetch +} + +// JwksFetch controls the server-side fetch of an external JWT signer's jwksEndpoint. +// The endpoint URL is supplied by an operator, so the fetch is constrained to keep it +// from being pointed at addresses the controller can reach but a caller should not. +// +// A hop is fetched only if it passes two independent gates. Neither gate can authorize what +// the other refuses, and both are applied to the initial request and to every redirect. +// +// The host gate is applied to the URL's hostname: +// +// 1. DeniedHostnames - blocked +// 2. AllowedHostnames, when non-empty and the host does not match - blocked +// 3. otherwise - passes +// +// The address gate is applied to the resolved address being connected to, first-match-wins, +// deny before allow: +// +// 1. built-in blocked addresses (cloud metadata, link-local, link-local multicast, +// unspecified) - always blocked, AllowedIPs cannot override +// 2. DeniedIPs - blocked, AllowedIPs cannot override +// 3. AllowedIPs - allowed; a carve-out of tier 4 only +// 4. BlockPrivateAddresses and the address is private or loopback - blocked +// 5. everything else - allowed +type JwksFetch struct { + // BlockPrivateAddresses blocks private and loopback addresses (address gate tier 4). + // Defaults to false so deployments with an internal IdP keep working; tier 1 applies + // regardless. + BlockPrivateAddresses bool + + // DeniedIPs are CIDRs that are always blocked (address gate tier 2), above + // AllowedIPs. + DeniedIPs []*net.IPNet + + // AllowedIPs are CIDRs that carve an exception out of BlockPrivateAddresses + // (address gate tier 3). They do not override tier 1 or DeniedIPs. + AllowedIPs []*net.IPNet + + // DeniedHostnames are normalized hostname patterns that are blocked (hostname gate tier 1). Host + // matching only ever narrows what may be fetched: it cannot authorize an address the + // address gate blocks, and a caller can still reach the same target under another name, + // so the address gate remains the boundary. + DeniedHostnames []string + + // AllowedHostnames are normalized hostname patterns that, when non-empty, are the only hosts that + // may be fetched (hostname gate tier 2). Entries are an exact hostname (idp.example.com) or a + // wildcard suffix (*.example.com), which matches any subdomain but not the suffix itself. + AllowedHostnames []string + + // Timeout bounds the total time spent on a single JWKS fetch, including redirects. + Timeout time.Duration + + // MaxRedirects bounds how many redirects a JWKS fetch will follow. Every hop is + // address-checked. Zero disables redirects. + MaxRedirects int +} + type EdgeConfig struct { Enabled bool Api Api @@ -149,6 +222,7 @@ type EdgeConfig struct { caCerts []*x509.Certificate caCertPool *x509.CertPool DisablePostureChecks bool + ExternalJwtSigners ExternalJwtSigners } type HttpTimeouts struct { @@ -182,10 +256,24 @@ type IdentityStatusConfig struct { UnknownTimeout time.Duration } +// DefaultJwksFetch returns the default JWKS fetch settings. The defaults are deliberately +// compatible with existing deployments: only the non-disableable built-in blocked addresses +// are refused. +func DefaultJwksFetch() JwksFetch { + return JwksFetch{ + BlockPrivateAddresses: DefaultJwksFetchBlockPrivateAddresses, + Timeout: DefaultJwksFetchTimeout, + MaxRedirects: DefaultJwksFetchMaxRedirects, + } +} + func NewEdgeConfig() *EdgeConfig { return &EdgeConfig{ Enabled: false, caPems: bytes.NewBuffer(nil), + ExternalJwtSigners: ExternalJwtSigners{ + JwksFetch: DefaultJwksFetch(), + }, } } @@ -703,6 +791,250 @@ func (c *EdgeConfig) loadIdentityStatusConfig(cfgmap map[interface{}]interface{} return nil } +// loadExternalJwtSignersSection loads [edge.externalJwtSigners]. Every value is optional; +// absent values keep the defaults from DefaultJwksFetch. +func (c *EdgeConfig) loadExternalJwtSignersSection(edgeConfigMap map[any]any) error { + c.ExternalJwtSigners.JwksFetch = DefaultJwksFetch() + + value, found := edgeConfigMap["externalJwtSigners"] + + if !found || value == nil { + return nil + } + + extJwtSignersMap, ok := value.(map[any]any) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners], must be a map", value) + } + + value, found = extJwtSignersMap["jwksFetch"] + + if !found || value == nil { + return nil + } + + jwksFetchMap, ok := value.(map[any]any) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch], must be a map", value) + } + + jwksFetch := &c.ExternalJwtSigners.JwksFetch + + if val, found := jwksFetchMap["blockPrivateAddresses"]; found && val != nil { + switch typedVal := val.(type) { + case bool: + jwksFetch.BlockPrivateAddresses = typedVal + case string: + boolVal, err := strconv.ParseBool(typedVal) + if err != nil { + return errors.Errorf("invalid value %q for [edge.externalJwtSigners.jwksFetch.blockPrivateAddresses], must be a boolean", typedVal) + } + jwksFetch.BlockPrivateAddresses = boolVal + default: + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch.blockPrivateAddresses], must be a boolean", val) + } + } + + if val, found := jwksFetchMap["deniedIPs"]; found && val != nil { + addresses, err := parseCidrList(val, "edge.externalJwtSigners.jwksFetch.deniedIPs") + if err != nil { + return err + } + jwksFetch.DeniedIPs = addresses + } + + if val, found := jwksFetchMap["allowedIPs"]; found && val != nil { + addresses, err := parseCidrList(val, "edge.externalJwtSigners.jwksFetch.allowedIPs") + if err != nil { + return err + } + jwksFetch.AllowedIPs = addresses + } + + if val, found := jwksFetchMap["deniedHostnames"]; found && val != nil { + hosts, err := parseHostnameList(val, "edge.externalJwtSigners.jwksFetch.deniedHostnames") + if err != nil { + return err + } + jwksFetch.DeniedHostnames = hosts + } + + if val, found := jwksFetchMap["allowedHostnames"]; found && val != nil { + hosts, err := parseHostnameList(val, "edge.externalJwtSigners.jwksFetch.allowedHostnames") + if err != nil { + return err + } + jwksFetch.AllowedHostnames = hosts + } + + if val, found := jwksFetchMap["timeout"]; found && val != nil { + strVal, ok := val.(string) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch.timeout], must be a string duration", val) + } + + durationVal, err := time.ParseDuration(strVal) + + if err != nil { + return errors.Errorf("error parsing [edge.externalJwtSigners.jwksFetch.timeout], invalid duration string %s, cannot parse as duration (e.g. 5s): %v", strVal, err) + } + + if durationVal <= 0 { + return errors.Errorf("invalid value %s for [edge.externalJwtSigners.jwksFetch.timeout], must be greater than zero", strVal) + } + + jwksFetch.Timeout = durationVal + } + + if val, found := jwksFetchMap["maxRedirects"]; found && val != nil { + intVal, ok := val.(int) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch.maxRedirects], must be an integer", val) + } + + if intVal < 0 { + return errors.Errorf("invalid value %v for [edge.externalJwtSigners.jwksFetch.maxRedirects], must not be negative", intVal) + } + + jwksFetch.MaxRedirects = intVal + } + + return nil +} + +// parseCidrList parses a list of CIDRs, accepting a bare IP address as a single address CIDR +// (/32 for IPv4, /128 for IPv6). Hostnames are rejected: the address check happens at dial +// time against the resolved IP, so a hostname entry could never be matched reliably. +func parseCidrList(value any, field string) ([]*net.IPNet, error) { + values, ok := value.([]any) + + if !ok { + return nil, errors.Errorf("invalid type %T for [%s], must be a list of CIDRs", value, field) + } + + var result []*net.IPNet + + for _, entry := range values { + strVal, ok := entry.(string) + + if !ok { + return nil, errors.Errorf("invalid type %T for an entry in [%s], must be a string CIDR", entry, field) + } + + ipNet, err := parseCidrOrIp(strings.TrimSpace(strVal)) + + if err != nil { + return nil, errors.Errorf("invalid value %q in [%s]: %v", strVal, field, err) + } + + result = append(result, ipNet) + } + + return result, nil +} + +// parseHostnameList parses a list of hostname patterns, returning them normalized for comparison +// against a URL's hostname. +func parseHostnameList(value any, field string) ([]string, error) { + values, ok := value.([]any) + + if !ok { + return nil, errors.Errorf("invalid type %T for [%s], must be a list of hostnames", value, field) + } + + var result []string + + for _, entry := range values { + strVal, ok := entry.(string) + + if !ok { + return nil, errors.Errorf("invalid type %T for an entry in [%s], must be a string hostname", entry, field) + } + + pattern, err := parseHostnamePattern(strings.TrimSpace(strVal)) + + if err != nil { + return nil, errors.Errorf("invalid value %q in [%s]: %v", strVal, field, err) + } + + result = append(result, pattern) + } + + return result, nil +} + +// parseHostnamePattern validates a host entry and returns it normalized. An entry is either an +// exact host (idp.example.com) or a wildcard suffix (*.example.com), which matches any +// subdomain of that suffix but not the suffix itself. IP addresses are rejected: matching an +// address by name comparison would not be an address check. +func parseHostnamePattern(value string) (string, error) { + if value == "" { + return "", errors.New("must not be empty") + } + + host := strings.TrimPrefix(value, "*.") + isWildcard := host != value + + if net.ParseIP(strings.Trim(host, "[]")) != nil { + return "", errors.New("must be a hostname, use deniedIPs or allowedIPs for IP addresses") + } + + if host == "" { + return "", errors.New("must include a hostname after the leading \"*.\"") + } + + if strings.ContainsAny(host, "*/:@ \t") { + return "", errors.New("must be a bare host name, without a scheme, port or path, and a wildcard is only supported as a leading \"*.\"") + } + + normalized := NormalizeHostname(host) + + if normalized == "" { + return "", errors.New("must be a valid host name") + } + + if isWildcard { + return "*." + normalized, nil + } + + return normalized, nil +} + +// NormalizeHostname returns a host in the form used for comparison: lower-cased, without a +// trailing dot, and converted to punycode when it contains non-ASCII labels. Config entries +// and request hosts are both normalized this way so that they compare consistently. +func NormalizeHostname(host string) string { + host = strings.TrimSuffix(strings.TrimSpace(host), ".") + + if ascii, err := idna.Lookup.ToASCII(host); err == nil { + host = ascii + } + + return strings.ToLower(host) +} + +// parseCidrOrIp parses a CIDR or a bare IP address into a *net.IPNet. A bare IP address +// becomes a single address CIDR. +func parseCidrOrIp(value string) (*net.IPNet, error) { + if _, ipNet, err := net.ParseCIDR(value); err == nil { + return ipNet, nil + } + + if ip := net.ParseIP(value); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + return &net.IPNet{IP: ip4, Mask: net.CIDRMask(32, 32)}, nil + } + + return &net.IPNet{IP: ip.To16(), Mask: net.CIDRMask(128, 128)}, nil + } + + return nil, errors.New("must be a CIDR (e.g. 10.0.0.0/8) or an IP address, hostnames are not supported") +} + func LoadEdgeConfigFromMap(configMap map[interface{}]interface{}) (*EdgeConfig, error) { edgeConfig := NewEdgeConfig() @@ -748,6 +1080,10 @@ func LoadEdgeConfigFromMap(configMap map[interface{}]interface{}) (*EdgeConfig, return nil, err } + if err = edgeConfig.loadExternalJwtSignersSection(edgeConfigMap); err != nil { + return nil, err + } + if v, ok := edgeConfigMap["disablePostureChecks"]; ok { if boolVal, ok := v.(bool); ok { edgeConfig.DisablePostureChecks = boolVal diff --git a/controller/config/config_edge_test.go b/controller/config/config_edge_test.go index b52fcd7af..9972c6085 100644 --- a/controller/config/config_edge_test.go +++ b/controller/config/config_edge_test.go @@ -362,6 +362,261 @@ func Test_CalculateCaPems(t *testing.T) { } +func Test_loadExternalJwtSignersSection(t *testing.T) { + t.Run("an absent section yields defaults", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{})) + + req.False(c.ExternalJwtSigners.JwksFetch.BlockPrivateAddresses) + req.Empty(c.ExternalJwtSigners.JwksFetch.DeniedIPs) + req.Empty(c.ExternalJwtSigners.JwksFetch.AllowedIPs) + req.Empty(c.ExternalJwtSigners.JwksFetch.DeniedHostnames) + req.Empty(c.ExternalJwtSigners.JwksFetch.AllowedHostnames) + req.Equal(DefaultJwksFetchTimeout, c.ExternalJwtSigners.JwksFetch.Timeout) + req.Equal(DefaultJwksFetchMaxRedirects, c.ExternalJwtSigners.JwksFetch.MaxRedirects) + }) + + t.Run("an absent jwksFetch sub-section yields defaults", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{}, + })) + + req.False(c.ExternalJwtSigners.JwksFetch.BlockPrivateAddresses) + req.Equal(DefaultJwksFetchTimeout, c.ExternalJwtSigners.JwksFetch.Timeout) + req.Equal(DefaultJwksFetchMaxRedirects, c.ExternalJwtSigners.JwksFetch.MaxRedirects) + }) + + t.Run("a fully specified section is parsed", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "blockPrivateAddresses": true, + "deniedIPs": []any{"10.0.0.0/8", "203.0.113.5"}, + "allowedIPs": []any{"192.168.10.0/24", "fd00:1234::1"}, + "timeout": "12s", + "maxRedirects": 2, + }, + }, + })) + + jwksFetch := c.ExternalJwtSigners.JwksFetch + + req.True(jwksFetch.BlockPrivateAddresses) + req.Equal(12*time.Second, jwksFetch.Timeout) + req.Equal(2, jwksFetch.MaxRedirects) + + req.Len(jwksFetch.DeniedIPs, 2) + req.Equal("10.0.0.0/8", jwksFetch.DeniedIPs[0].String()) + req.Equal("203.0.113.5/32", jwksFetch.DeniedIPs[1].String(), "a bare IPv4 address should be treated as a /32") + + req.Len(jwksFetch.AllowedIPs, 2) + req.Equal("192.168.10.0/24", jwksFetch.AllowedIPs[0].String()) + req.Equal("fd00:1234::1/128", jwksFetch.AllowedIPs[1].String(), "a bare IPv6 address should be treated as a /128") + }) + + t.Run("host lists are parsed and normalized", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedHostnames": []any{"Blocked.Example.COM", "internal.example.com."}, + "allowedHostnames": []any{"idp.example.com", "*.idp.example.org"}, + }, + }, + })) + + jwksFetch := c.ExternalJwtSigners.JwksFetch + + req.Equal([]string{"blocked.example.com", "internal.example.com"}, jwksFetch.DeniedHostnames, + "host entries should be lower-cased with any trailing dot removed") + req.Equal([]string{"idp.example.com", "*.idp.example.org"}, jwksFetch.AllowedHostnames) + }) + + t.Run("a host list entry that is an IP address is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedHostnames": []any{"10.0.0.5"}, + }, + }, + }) + + req.Error(err, "an IP address in a host list would be a name comparison, not an address check") + req.Contains(err.Error(), "deniedIPs") + }) + + t.Run("a host list entry with a scheme, port or path is an error", func(t *testing.T) { + entries := []string{"https://idp.example.com", "idp.example.com:443", "idp.example.com/jwks", "idp.*.example.com", "*.", ""} + + for _, entry := range entries { + t.Run(entry, func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "allowedHostnames": []any{entry}, + }, + }, + }) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + }) + } + }) + + t.Run("blockPrivateAddresses accepts a string boolean", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "blockPrivateAddresses": "true", + }, + }, + })) + + req.True(c.ExternalJwtSigners.JwksFetch.BlockPrivateAddresses) + }) + + t.Run("maxRedirects of zero is allowed and disables redirects", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "maxRedirects": 0, + }, + }, + })) + + req.Equal(0, c.ExternalJwtSigners.JwksFetch.MaxRedirects) + }) + + t.Run("an invalid CIDR is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedIPs": []any{"not-an-address"}, + }, + }, + }) + + req.Error(err) + req.Contains(err.Error(), "deniedIPs") + }) + + t.Run("a hostname entry is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "allowedIPs": []any{"idp.example.com"}, + }, + }, + }) + + req.Error(err, "hostnames are not valid entries, they cannot be enforced at dial time") + req.Contains(err.Error(), "allowedIPs") + }) + + t.Run("a non-list address value is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedIPs": "10.0.0.0/8", + }, + }, + }) + + req.Error(err) + }) + + t.Run("an invalid duration is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "timeout": "not-a-duration", + }, + }, + }) + + req.Error(err) + }) + + t.Run("a non-positive timeout is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "timeout": "0s", + }, + }, + }) + + req.Error(err, "an unbounded fetch is exactly what this section exists to prevent") + }) + + t.Run("a negative maxRedirects is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "maxRedirects": -1, + }, + }, + }) + + req.Error(err) + }) +} + func newSelfSignedCert(commonName string, isCas bool) (*x509.Certificate, crypto.PrivateKey) { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { diff --git a/controller/config/config_network.go b/controller/config/config_network.go index 8a2a9e44c..66eb67bee 100644 --- a/controller/config/config_network.go +++ b/controller/config/config_network.go @@ -46,14 +46,22 @@ const ( ) type NetworkConfig struct { - CreateCircuitRetries uint32 - CycleSeconds uint32 - InitialLinkLatency time.Duration - IntervalAgeThreshold time.Duration - MetricsReportInterval time.Duration - MinRouterCost uint16 - PendingLinkTimeout time.Duration - RouteTimeout time.Duration + CreateCircuitRetries uint32 + CycleSeconds uint32 + InitialLinkLatency time.Duration + IntervalAgeThreshold time.Duration + MetricsReportInterval time.Duration + MinRouterCost uint16 + PendingLinkTimeout time.Duration + RouteTimeout time.Duration + // RouterConnectChurnLimit is how long an established router control channel is protected from being + // displaced by a new connection for the same router. A new connection arriving inside the window is + // refused; after it, the established connection is displaced and the router redials into the freed + // slot. Zero always allows takeover. + // + // This is churn policy, not the guarantee that a router has one connection: that is enforced under the + // per-router lock in Network.ConnectRouter. Its purpose is to stop a flapping router from repeatedly + // tearing down a working channel, since displacement is not free. RouterConnectChurnLimit time.Duration RouterComm struct { QueueSize uint32 diff --git a/controller/controller.go b/controller/controller.go index 9f584c5f8..8fb08ef30 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -33,6 +33,7 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/channel/v4/protobufs" "github.com/openziti/foundation/v2/concurrenz" + nfpem "github.com/openziti/foundation/v2/pem" "github.com/openziti/foundation/v2/versions" "github.com/openziti/identity" "github.com/openziti/metrics" @@ -68,6 +69,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/teris-io/shortid" + "go.etcd.io/bbolt" ) type Controller struct { @@ -311,8 +313,15 @@ func NewController(cfg *config.Config, versionProvider versions.VersionProvider) c.initWeb() // need to init web before bootstrapping, so we can provide our endpoints to peers - if c.raftController != nil && !c.raftController.IsBootstrapped() { - if err = c.TryInitializeRaftFromBoltDb(); err != nil { + if c.raftController != nil { + _, dbConfigured := c.config.Src["db"] + if c.raftController.IsBootstrapped() { + // On a clustered config 'db' only seeds a new cluster on first bootstrap; once + // initialized it is dead config, so warn rather than let a stale setting sit unnoticed. + if dbConfigured { + log.Warn("'db' is set but this clustered controller is already initialized; the 'db' setting is ignored and should be removed from the configuration") + } + } else if err = c.TryInitializeRaftFromBoltDb(); err != nil { log.WithError(err).Panic("error bootstrapping raft") } } @@ -537,9 +546,6 @@ func (c *Controller) Run() error { pfxlog.Logger().Infof("staring control channel listener on %s", c.config.Ctrl.Listener.String()) ctrlListener := channel.NewClassicListener(c.config.Id, c.config.Ctrl.Listener, ctrlChannelListenerConfig) c.ctrlListener = ctrlListener - if err := c.ctrlListener.Listen(c.ctrlConnectHandler); err != nil { - panic(err) - } ctrlAccepter := handler_ctrl.NewCtrlAccepter(c.network, c.xctrls, c.config.Ctrl.Options.Options, c.config.Ctrl.Options.RouterHeartbeatOptions, c.config.Trace.Handler) @@ -549,6 +555,19 @@ func (c *Controller) Run() error { ctrlAcceptors[mesh.ChannelTypeMesh] = c.raftController.GetMesh() } + // Channel types routed to a dedicated acceptor above validate their own peers; the connect handler + // must skip exactly those and validate everything else (which the dispatcher routes to the default + // router control acceptor, including unrecognized types). Set this before accepting connections. + separatelyValidatedTypes := map[string]struct{}{} + for chType := range ctrlAcceptors { + separatelyValidatedTypes[chType] = struct{}{} + } + c.ctrlConnectHandler.SetSeparatelyValidatedChannelTypes(separatelyValidatedTypes) + + if err := c.ctrlListener.Listen(c.ctrlConnectHandler); err != nil { + panic(err) + } + underlayDispatcher := channel.NewUnderlayDispatcher(channel.UnderlayDispatcherConfig{ Listener: ctrlListener, ConnectTimeout: c.config.Ctrl.Options.ConnectTimeout, @@ -699,11 +718,21 @@ func (c *Controller) registerXts() { } func (c *Controller) registerComponents() error { - c.ctrlConnectHandler = handler_ctrl.NewConnectHandler(c.config.Id, c.network) + c.ctrlConnectHandler = handler_ctrl.NewConnectHandler(c.config.Id, c.network, c.signingCertRoots()) c.eventDispatcher.AddClusterEventHandler(event.ClusterEventHandlerF(c.routerDispatchCallback)) return nil } +// signingCertRoots returns the trust anchors from the edge enrollment signing CA bundle, which issues the +// certificates routers present on the control channel. It is empty when no signing CA bundle is +// configured, in which case a router is trusted only through the controller's own CA bundle. +func (c *Controller) signingCertRoots() []*x509.Certificate { + if c.config.Edge == nil { + return nil + } + return nfpem.PemBytesToCertificates(c.config.Edge.Enrollment.SigningCertCaPem) +} + func (c *Controller) RegisterXctrl(x xctrl.Xctrl) error { if err := c.config.Configure(x); err != nil { return err @@ -842,6 +871,32 @@ func (c *Controller) InitializeRaftFromBoltDb(sourceDbPath string) error { return c.RaftRestoreFromBoltDb(sourceDbPath) } +// validateMigrationSourceDb rejects a migration source that is not an initialized controller db. +// db.Open creates the root bucket on any file, so existence is not enough; it checks for a default +// admin identity, which an initialized controller always has. The check is read-only. +func validateMigrationSourceDb(sourceDb boltz.Db) error { + hasDefaultAdmin := false + err := sourceDb.View(func(tx *bbolt.Tx) error { + identities := boltz.Path(tx, db.RootBucket, db.EntityTypeIdentities) + if identities == nil { + return nil + } + return identities.ForEachTypedBucket(func(_ string, identity *boltz.TypedBucket) error { + if identity.GetBoolWithDefault(db.FieldIdentityIsDefaultAdmin, false) { + hasDefaultAdmin = true + } + return nil + }) + }) + if err != nil { + return errors.Wrap(err, "unable to read identities from source db") + } + if !hasDefaultAdmin { + return errors.New("source db has no default admin identity; it is empty or was never a fully initialized controller") + } + return nil +} + func (c *Controller) RaftRestoreFromBoltDb(sourceDbPath string) error { log := pfxlog.Logger() @@ -866,6 +921,10 @@ func (c *Controller) RaftRestoreFromBoltDb(sourceDbPath string) error { } }() + if err = validateMigrationSourceDb(sourceDb); err != nil { + return errors.Wrapf(err, "migration source db [%v] is not a valid initialized controller database", sourceDbPath) + } + timelineId, err := sourceDb.GetTimelineId(boltz.TimelineModeForceReset, shortid.Generate) if err != nil { return err @@ -895,6 +954,13 @@ func (c *Controller) RaftRestoreFromBoltDb(sourceDbPath string) error { return fmt.Errorf("unable to bootstrap cluster (%w)", err) } + // Carry the cluster id Bootstrap established so RestoreSnapshot can write it back after the + // restore (the migration source has none). Blank here means a bug in Bootstrap, so fail. + cmd.ClusterId = c.raftController.GetClusterId() + if cmd.ClusterId == "" { + return errors.New("cluster id is blank after bootstrap; refusing to restore without a durable cluster id") + } + return c.raftController.Dispatch(cmd) } diff --git a/controller/controller_test.go b/controller/controller_test.go new file mode 100644 index 000000000..0e696cd3e --- /dev/null +++ b/controller/controller_test.go @@ -0,0 +1,66 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package controller + +import ( + "path/filepath" + "testing" + + "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/storage/boltz" + "github.com/stretchr/testify/require" +) + +func TestValidateMigrationSourceDb(t *testing.T) { + t.Run("rejects a db with no identities", func(t *testing.T) { + // db.Open creates the root bucket but no identities, mimicking an empty or stray file. + sourceDb, err := db.Open(filepath.Join(t.TempDir(), "empty.db")) + require.NoError(t, err) + defer func() { _ = sourceDb.Close() }() + + require.Error(t, validateMigrationSourceDb(sourceDb)) + }) + + t.Run("rejects a db whose identities include no default admin", func(t *testing.T) { + sourceDb, err := db.Open(filepath.Join(t.TempDir(), "no-admin.db")) + require.NoError(t, err) + defer func() { _ = sourceDb.Close() }() + + require.NoError(t, addIdentity(sourceDb, "regular-identity", false)) + require.Error(t, validateMigrationSourceDb(sourceDb)) + }) + + t.Run("accepts a db that has a default admin identity", func(t *testing.T) { + sourceDb, err := db.Open(filepath.Join(t.TempDir(), "populated.db")) + require.NoError(t, err) + defer func() { _ = sourceDb.Close() }() + + require.NoError(t, addIdentity(sourceDb, "regular-identity", false)) + require.NoError(t, addIdentity(sourceDb, "admin-identity", true)) + require.NoError(t, validateMigrationSourceDb(sourceDb)) + }) +} + +// addIdentity writes an identity bucket with the isDefaultAdmin field set, using boltz so the +// stored value is read back the same way the controller reads it. +func addIdentity(sourceDb boltz.Db, id string, isDefaultAdmin bool) error { + return sourceDb.Update(nil, func(ctx boltz.MutateContext) error { + idBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.EntityTypeIdentities, id) + idBucket.SetBool(db.FieldIdentityIsDefaultAdmin, isDefaultAdmin, nil) + return idBucket.GetError() + }) +} diff --git a/controller/db/db.go b/controller/db/db.go index 8a448d83d..9a06bfd9a 100644 --- a/controller/db/db.go +++ b/controller/db/db.go @@ -17,8 +17,7 @@ package db import ( - "fmt" - + "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/v2/controller/storage/boltz" "go.etcd.io/bbolt" ) @@ -74,20 +73,29 @@ func LoadClusterId(db boltz.Db) (string, error) { return result, err } -func InitClusterId(db boltz.Db, ctx boltz.MutateContext, clusterId string) error { - return db.Update(ctx, func(ctx boltz.MutateContext) error { +// InitClusterId sets the cluster id if unset and returns the effective id (an existing id wins). It +// is set-once: a differing id is kept with a warning rather than an error, so redundant or racing +// writes (e.g. backfill across leadership changes) cannot fail. +func InitClusterId(db boltz.Db, ctx boltz.MutateContext, clusterId string) (string, error) { + effective := clusterId + err := db.Update(ctx, func(ctx boltz.MutateContext) error { raftBucket := boltz.GetOrCreatePath(ctx.Tx(), RootBucket, MetadataBucket) if raftBucket.HasError() { return raftBucket.Err } currentId := raftBucket.GetStringWithDefault(FieldClusterId, "") if currentId != "" { - if currentId == clusterId { - return nil + effective = currentId + if currentId != clusterId { + pfxlog.Logger(). + WithField("existingClusterId", currentId). + WithField("ignoredClusterId", clusterId). + Warn("cluster id already set; keeping existing value and ignoring the new one") } - return fmt.Errorf("cluster id already initialized to %s", currentId) + return nil } raftBucket.SetString(FieldClusterId, clusterId, nil) return raftBucket.Err }) + return effective, err } diff --git a/controller/db/migration_initialize.go b/controller/db/migration_initialize.go index 58630a639..97fef1b86 100644 --- a/controller/db/migration_initialize.go +++ b/controller/db/migration_initialize.go @@ -45,6 +45,8 @@ func (m *Migrations) initialize(step *boltz.MigrationStep) int { m.addSystemAuthPolicies(step) m.createConfigType(step, interfacesConfigTypeV1) m.createConfigType(step, proxyConfigTypeV1) + m.createConfigType(step, l2HostV1ConfigType) + m.createConfigType(step, l2InterceptV1ConfigType) return CurrentDbVersion } @@ -321,6 +323,65 @@ var tunnelDefinitions = map[string]interface{}{ }, } +var listenOptions = map[string]interface{}{ + "listenOptions": map[string]interface{}{ + "type": "object", + "additionalProperties": false, + "properties": map[string]interface{}{ + "connectTimeoutSeconds": map[string]interface{}{ + "$ref": "#/definitions/timeoutSeconds", + "description": "Timeout when making outbound connections. Defaults to 5. If both connectTimeoutSeconds and connectTimeout are specified, connectTimeout will be used.", + "deprecated": true, + }, + "connectTimeout": map[string]interface{}{ + "$ref": "#/definitions/duration", + "description": "Timeout when making outbound connections. Defaults to '5s'. If both connectTimeoutSeconds and connectTimeout are specified, connectTimeout will be used.", + }, + "maxConnections": map[string]interface{}{ + "type": "integer", + "minimum": 1, + "description": "defaults to 3", + }, + "identity": map[string]interface{}{ + "type": "string", + "description": "Associate the hosting terminator with the specified identity. '$tunneler_id.name' resolves to the name of the hosting tunneler's identity. '$tunneler_id.tag[tagName]' resolves to the value of the 'tagName' tag on the hosting tunneler's identity.", + }, + "bindUsingEdgeIdentity": map[string]interface{}{ + "type": "boolean", + "description": "Associate the hosting terminator with the name of the hosting tunneler's identity. Setting this to 'true' is equivalent to setting 'identiy=$tunneler_id.name'", + }, + "cost": map[string]interface{}{ + "type": "integer", + "minimum": 0, + "maximum": 65535, + "description": "defaults to 0", + }, + "precedence": map[string]interface{}{ + "type": "string", + "enum": []interface{}{"default", "required", "failed"}, + "description": "defaults to 'default'", + }, + }, + }, +} + +var dialOptions = map[string]interface{}{ + "dialOptions": map[string]interface{}{ + "type": "object", + "additionalProperties": false, + "properties": map[string]interface{}{ + "identity": map[string]interface{}{ + "type": "string", + "description": "Dial a terminator with the specified identity. '$dst_protocol', '$dst_ip', '$dst_port are resolved to the corresponding value of the destination address.", + }, + "connectTimeoutSeconds": map[string]interface{}{ + "$ref": "#/definitions/timeoutSeconds", + "description": "defaults to 5 seconds if no dialOptions are defined. defaults to 15 if dialOptions are defined but connectTimeoutSeconds is not specified.", + }, + }, + }, +} + // hostV1 schema with ["$id"] and ["definitions"] excluded var hostV1SchemaSansDefs = map[string]interface{}{ "type": "object", @@ -388,50 +449,12 @@ var hostV1SchemaSansDefs = map[string]interface{}{ }, "description": "hosting tunnelers establish local routes for the specified source addresses so binding will succeed", }, - "listenOptions": map[string]interface{}{ - "type": "object", - "additionalProperties": false, - "properties": map[string]interface{}{ - "connectTimeoutSeconds": map[string]interface{}{ - "$ref": "#/definitions/timeoutSeconds", - "description": "Timeout when making outbound connections. Defaults to 5. If both connectTimoutSeconds and connectTimeout are specified, connectTimeout will be used.", - "deprecated": true, - }, - "connectTimeout": map[string]interface{}{ - "$ref": "#/definitions/duration", - "description": "Timeout when making outbound connections. Defaults to '5s'. If both connectTimoutSeconds and connectTimeout are specified, connectTimeout will be used.", - }, - "maxConnections": map[string]interface{}{ - "type": "integer", - "minimum": 1, - "description": "defaults to 3", - }, - "identity": map[string]interface{}{ - "type": "string", - "description": "Associate the hosting terminator with the specified identity. '$tunneler_id.name' resolves to the name of the hosting tunneler's identity. '$tunneler_id.tag[tagName]' resolves to the value of the 'tagName' tag on the hosting tunneler's identity.", - }, - "bindUsingEdgeIdentity": map[string]interface{}{ - "type": "boolean", - "description": "Associate the hosting terminator with the name of the hosting tunneler's identity. Setting this to 'true' is equivalent to setting 'identiy=$tunneler_id.name'", - }, - "cost": map[string]interface{}{ - "type": "integer", - "minimum": 0, - "maximum": 65535, - "description": "defaults to 0", - }, - "precedence": map[string]interface{}{ - "type": "string", - "enum": []interface{}{"default", "required", "failed"}, - "description": "defaults to 'default'", - }, - }, - }, "proxy": map[string]interface{}{ "$ref": "#/definitions/proxyConfiguration", "description": "If defined, outgoing connections will be send through this proxy server", }, }, + listenOptions, ), "additionalProperties": false, "allOf": []interface{}{ @@ -532,7 +555,7 @@ var interceptV1ConfigType = &ConfigType{ "type": "object", "additionalProperties": false, "definitions": tunnelDefinitions, - "properties": map[string]interface{}{ + "properties": combine(dialOptions, map[string]interface{}{ "protocols": map[string]interface{}{ "allOf": []interface{}{ map[string]interface{}{"$ref": "#/definitions/inhabitedSet"}, @@ -551,20 +574,6 @@ var interceptV1ConfigType = &ConfigType{ map[string]interface{}{"items": map[string]interface{}{"$ref": "#/definitions/portRange"}}, }, }, - "dialOptions": map[string]interface{}{ - "type": "object", - "additionalProperties": false, - "properties": map[string]interface{}{ - "identity": map[string]interface{}{ - "type": "string", - "description": "Dial a terminator with the specified identity. '$dst_protocol', '$dst_ip', '$dst_port are resolved to the corresponding value of the destination address.", - }, - "connectTimeoutSeconds": map[string]interface{}{ - "$ref": "#/definitions/timeoutSeconds", - "description": "defaults to 5 seconds if no dialOptions are defined. defaults to 15 if dialOptions are defined but connectTimeoutSeconds is not specified.", - }, - }, - }, "sourceIp": map[string]interface{}{ "type": "string", "description": "The source IP (and optional :port) to spoof when the connection is egressed from the hosting tunneler. '$tunneler_id.name' resolves to the name of the client tunneler's identity. '$tunneler_id.tag[tagName]' resolves to the value of the 'tagName' tag on the client tunneler's identity. '$src_ip' and '$src_port' resolve to the source IP / port of the originating client. '$dst_port' resolves to the port that the client is trying to connect.", @@ -576,7 +585,7 @@ var interceptV1ConfigType = &ConfigType{ }, "description": "white list of source ips/cidrs that can be intercepted. all ips can be intercepted if this is not set.", }, - }, + }), "required": []interface{}{ "protocols", "addresses", @@ -585,6 +594,54 @@ var interceptV1ConfigType = &ConfigType{ }, } +var l2HostV1ConfigType = &ConfigType{ + BaseExtEntity: boltz.BaseExtEntity{Id: "l2.host.v1"}, + Name: "l2.host.v1", + Schema: map[string]interface{}{ + "$id": "https://ziti-edge.netfoundry.io/schemas/l2.host.v1.schema.json", + "definitions": combine(healthCheckSchema["definitions"].(map[string]interface{}), tunnelDefinitions), + "type": "object", + "properties": combine(listenOptions, map[string]interface{}{ + "bridgeIfs": map[string]interface{}{ + "allOf": []interface{}{ + map[string]interface{}{"$ref": "#/definitions/inhabitedSet"}, + map[string]interface{}{"items": map[string]interface{}{"type": "string"}}, + }, + "description": "Bridge the provided network interfaces with the tunneler's tap interface.", + }, + }), + "additionalProperties": false, + }, +} + +var l2InterceptV1ConfigType = &ConfigType{ + BaseExtEntity: boltz.BaseExtEntity{Id: "l2.intercept.v1"}, + Name: "l2.intercept.v1", + Schema: map[string]interface{}{ + "$id": "https://ziti-edge.netfoundry.io/schemas/l2.intercept.v1.schema.json", + "definitions": combine(tunnelDefinitions, map[string]interface{}{ + "ethType": map[string]interface{}{ + "type": "string", + "pattern": "^0[xX][0-9a-fA-F]{4}$", + }, + }), + "type": "object", + "properties": combine(dialOptions, map[string]interface{}{ + "ethTypes": map[string]interface{}{ + "allOf": []interface{}{ + map[string]interface{}{"$ref": "#/definitions/inhabitedSet"}, + map[string]interface{}{"items": map[string]interface{}{"$ref": "#/definitions/ethType"}}, + }, + "description": "list of EtherTypes to forward. frames with an EtherType that is not in this list will be dropped.", + }, + }), + "required": []interface{}{ + "ethTypes", + }, + "additionalProperties": false, + }, +} + var InterfacesV1TypeId = "interfaces.v1" var interfacesConfigTypeV1 = &ConfigType{ diff --git a/controller/db/migrations.go b/controller/db/migrations.go index e031375d9..e14ff8bc7 100644 --- a/controller/db/migrations.go +++ b/controller/db/migrations.go @@ -41,7 +41,21 @@ func RunMigrations(db boltz.Db, stores *Stores, signingCert *x509.Certificate) e } mm := boltz.NewMigratorManager(db) - return mm.Migrate("edge", CurrentDbVersion, migrations.migrate) + if err := mm.Migrate("edge", CurrentDbVersion, migrations.migrate); err != nil { + return err + } + + // l2 config types are needed in 2.0.x, but we don't want to increment the db version to add them, + // since doing so risks colliding with the db version range used by other branches. Since bumping + // the db version is what would normally trigger the migrator to invoke migrate() again, we instead + // ensure these config types exist on every startup, independent of the stored db version. + return db.Update(nil, func(ctx boltz.MutateContext) error { + step := &boltz.MigrationStep{Component: "edge", Ctx: ctx} + for _, cfgType := range []*ConfigType{l2HostV1ConfigType, l2InterceptV1ConfigType} { + migrations.createConfigType(step, cfgType) + } + return step.GetError() + }) } func (m *Migrations) migrate(step *boltz.MigrationStep) int { diff --git a/controller/db/service_policy_store_test.go b/controller/db/service_policy_store_test.go index 02d5317d7..4ab3b5fe7 100644 --- a/controller/db/service_policy_store_test.go +++ b/controller/db/service_policy_store_test.go @@ -24,6 +24,35 @@ func Test_ServicePolicyStore(t *testing.T) { t.Run("test service policy evaluation", ctx.testServicePolicyRoleEvaluation) t.Run("test update/delete referenced entities", ctx.testServicePolicyUpdateDeleteRefs) t.Run("test filter service policies by type name", ctx.testServicePolicyFilterByTypeName) + t.Run("test service policy enforcer type query", ctx.testServicePolicyEnforcerTypeQuery) +} + +// testServicePolicyEnforcerTypeQuery guards the predicate the ServicePolicyEnforcer uses to decide +// whether a session's identity still has a covering policy. The policy type symbol is string-mapped +// ("Dial"/"Bind"), so the predicate must filter on the string form; filtering on the numeric id +// (type = 1) matches nothing against the string symbol, which previously made the enforcer delete +// valid legacy sessions on startup. +func (ctx *TestContext) testServicePolicyEnforcerTypeQuery(_ *testing.T) { + ctx.CleanupAll() + + dialer := ctx.RequireNewIdentity(eid.New(), false) + service := newEdgeService(eid.New()) + boltztest.RequireCreate(ctx, service) + ctx.requireNewServicePolicy(PolicyTypeDial, ss(entityRef(dialer.Id)), ss(entityRef(service.Id))) + + ctx.NoError(ctx.GetDb().View(func(tx *bbolt.Tx) error { + matched, _, err := ctx.stores.Identity.QueryIds(tx, fmt.Sprintf( + `id = "%v" and not isEmpty(from servicePolicies where type = "%v" and anyOf(services) = "%v")`, + dialer.Id, PolicyTypeDial.String(), service.Id)) + ctx.NoError(err) + ctx.Contains(matched, dialer.Id, "string policy-type predicate must match the covering Dial policy") + + numeric, _, _ := ctx.stores.Identity.QueryIds(tx, fmt.Sprintf( + `id = "%v" and not isEmpty(from servicePolicies where type = %v and anyOf(services) = "%v")`, + dialer.Id, PolicyTypeDial.Id(), service.Id)) + ctx.NotContains(numeric, dialer.Id, "numeric policy-type predicate must not match the string-mapped symbol") + return nil + })) } func newServicePolicy(name string) *ServicePolicy { diff --git a/controller/env/appenv.go b/controller/env/appenv.go index 08f5a1cfb..2b158a025 100644 --- a/controller/env/appenv.go +++ b/controller/env/appenv.go @@ -50,13 +50,13 @@ import ( "github.com/openziti/identity" "github.com/openziti/metrics" "github.com/openziti/sdk-golang/ziti" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/xweb/v3" "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/common/cert" "github.com/openziti/ziti/v2/common/eid" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" "github.com/openziti/ziti/v2/controller/api" + "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/command" "github.com/openziti/ziti/v2/controller/config" "github.com/openziti/ziti/v2/controller/db" @@ -64,6 +64,7 @@ import ( "github.com/openziti/ziti/v2/controller/events" "github.com/openziti/ziti/v2/controller/jwtsigner" "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/network" "github.com/openziti/ziti/v2/controller/permissions" @@ -254,19 +255,19 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) parsedToken, err := jwt.ParseWithClaims(token, serviceAccessClaims, ae.JwtSignerKeyFunc) if err != nil { - return nil, err + return nil, &common.InvalidTokenError{Err: err} } if !parsedToken.Valid { - return nil, errors.New("service access token is invalid") + return nil, &common.InvalidTokenError{Err: errors.New("service access token is invalid")} } if !serviceAccessClaims.HasAudience(common.ClaimAudienceOpenZiti) && !serviceAccessClaims.HasAudience(common.ClaimLegacyNative) { - return nil, fmt.Errorf("invalid audience, expected an instance of %s or %s, got %v", common.ClaimAudienceOpenZiti, common.ClaimLegacyNative, serviceAccessClaims.Audience) + return nil, &common.InvalidTokenError{Err: fmt.Errorf("invalid audience, expected an instance of %s or %s, got %v", common.ClaimAudienceOpenZiti, common.ClaimLegacyNative, serviceAccessClaims.Audience)} } if serviceAccessClaims.TokenType != common.TokenTypeServiceAccess { - return nil, fmt.Errorf("invalid token type, expected %s, got %s", common.TokenTypeServiceAccess, serviceAccessClaims.Type) + return nil, &common.InvalidTokenError{Err: fmt.Errorf("invalid token type, expected %s, got %s", common.TokenTypeServiceAccess, serviceAccessClaims.Type)} } if apiSessionId != nil { @@ -275,10 +276,12 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) } if serviceAccessClaims.ApiSessionId != *apiSessionId { - return nil, fmt.Errorf("invalid api session id, expected %s, got %s", *apiSessionId, serviceAccessClaims.ApiSessionId) + return nil, &common.InvalidTokenError{Err: fmt.Errorf("invalid api session id, expected %s, got %s", *apiSessionId, serviceAccessClaims.ApiSessionId)} } } + // Revocation.Read failures below are infrastructure errors and are returned raw (not wrapped as + // InvalidTokenError), so a transient datastore failure does not make callers discard valid sessions. tokenRevocation, err := ae.GetManagers().Revocation.Read(serviceAccessClaims.ID) if err != nil && !boltz.IsErrNotFoundErr(err) { @@ -286,7 +289,7 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) } if tokenRevocation != nil { - return nil, errors.New("service access token has been revoked by id") + return nil, &common.InvalidTokenError{Err: errors.New("service access token has been revoked by id")} } revocation, err := ae.GetManagers().Revocation.Read(serviceAccessClaims.IdentityId) @@ -296,7 +299,7 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) } if revocation != nil && revocation.CreatedAt.After(serviceAccessClaims.IssuedAt.Time) { - return nil, errors.New("service access token has been revoked by identity") + return nil, &common.InvalidTokenError{Err: errors.New("service access token has been revoked by identity")} } return serviceAccessClaims, nil @@ -879,11 +882,27 @@ func (ae *AppEnv) GetControllerPublicKey(kid string) crypto.PublicKey { return signers[kid] } -// CreateRequestContext creates a new request context for handling HTTP requests. +// CreateRequestContext creates a new request context for handling HTTP requests. The request body +// is buffered into memory before any authentication check, so bodies larger than +// api.MaxRequestBodySize are rejected with a 413 ApiError instead of being buffered. func (ae *AppEnv) CreateRequestContext(rw http.ResponseWriter, r *http.Request) (*response.RequestContext, error) { rid := eid.New() - body, _ := io.ReadAll(r.Body) + if r.ContentLength > api.MaxRequestBodySize { + return nil, apierror.NewRequestEntityTooLarge() + } + + r.Body = http.MaxBytesReader(rw, r.Body, api.MaxRequestBodySize) + body, err := io.ReadAll(r.Body) + + if err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return nil, apierror.NewRequestEntityTooLarge() + } + return nil, apierror.NewCouldNotReadBody(err) + } + r.Body = io.NopCloser(bytes.NewReader(body)) securityTokenCtx, err := common.NewSecurityTokenCtx(r, ae.TokenIssuerCache) diff --git a/controller/env/context.go b/controller/env/context.go index 6ee8ec041..a0b6157d4 100644 --- a/controller/env/context.go +++ b/controller/env/context.go @@ -6,12 +6,17 @@ import ( "net/http" "github.com/openziti/ziti/v2/common/eid" + "github.com/openziti/ziti/v2/controller/api" "github.com/openziti/ziti/v2/controller/response" ) +// NewRequestContext creates a bare request context for responses rendered outside the normal +// request pipeline. The body read is capped at api.MaxRequestBodySize; a body that exceeds the +// cap is truncated rather than rejected, since this context only renders error responses. func NewRequestContext(rw http.ResponseWriter, r *http.Request) *response.RequestContext { rid := eid.New() + r.Body = http.MaxBytesReader(rw, r.Body, api.MaxRequestBodySize) body, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewReader(body)) diff --git a/controller/handler_ctrl/accept.go b/controller/handler_ctrl/accept.go index 529332fd3..e3b7f7625 100644 --- a/controller/handler_ctrl/accept.go +++ b/controller/handler_ctrl/accept.go @@ -17,8 +17,6 @@ package handler_ctrl import ( - "time" - "github.com/google/uuid" "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" @@ -111,16 +109,12 @@ func (self *CtrlAccepter) Bind(binding channel.Binding) error { ch := binding.GetChannel() log := pfxlog.Logger().WithField("routerId", ch.Id()) - // Use a new copy of the router instance each time we connect. That way we can tell on disconnect - // if we're working with the right connection, in case connects and disconnects happen quickly. - // It also means that the channel and connected time fields don't change and we don't have to protect them - r, err := self.network.GetReloadedRouter(ch.Id()) + // A fresh instance per connection, carrying this channel: that is what lets connect and disconnect tell + // two connections for one router apart, and keeps them from writing over each other's state. + r, err := self.network.NewCtrlChanRouter(ch) if err != nil { return err } - if r == nil { - return errors.Errorf("no router with id [%v] found, closing connection", ch.Id()) - } var ctrlChanListeners map[string][]string @@ -190,8 +184,6 @@ func (self *CtrlAccepter) Bind(binding channel.Binding) error { return errors.New("channel provided no headers, not accepting router connection as version info not provided") } - r.Control = ch.(channel.MultiChannel).GetUnderlayHandler().(ctrlchan.CtrlChannel) - r.ConnectTime = time.Now() if err := binding.Bind(newBindHandler(self.heartbeatOptions, r, self.network, self.xctrls)); err != nil { return errors.Wrap(err, "error binding router") } @@ -200,9 +192,16 @@ func (self *CtrlAccepter) Bind(binding channel.Binding) error { binding.AddPeekHandler(self.traceHandler) } - log.Info("accepted new router connection") + if err = self.network.ConnectRouter(r); err != nil { + if network.IsConnectRejected(err) { + log.Info("router connect rejected; another connection is already current, router will redial") + } + // Returning the error fails the bind, so NewChannel closes this channel's underlay without + // starting rx or registering it. That preserves the rx-gate for a rejected connection. + return err + } - self.network.ConnectRouter(r) + log.Info("accepted new router connection") return nil } diff --git a/controller/handler_ctrl/base.go b/controller/handler_ctrl/base.go index 547d04721..da180f2f0 100644 --- a/controller/handler_ctrl/base.go +++ b/controller/handler_ctrl/base.go @@ -17,10 +17,12 @@ package handler_ctrl import ( + "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/model" "github.com/openziti/ziti/v2/controller/network" + "github.com/openziti/ziti/v2/controller/storage/boltz" ) type baseHandler struct { @@ -31,3 +33,55 @@ type baseHandler struct { func (self *baseHandler) newChangeContext(ch channel.Channel, method string) *change.Context { return change.NewControlChannelChange(self.router.Id, self.router.Name, method, ch) } + +// ownsTerminator reports whether terminator belongs to the router on the other end of this handler's +// control channel. Terminator operations arriving on the fabric control channel are scoped to the +// requesting router, mirroring the edge control channel's verifyTerminator. +func (self *baseHandler) ownsTerminator(terminator *model.Terminator) bool { + return terminator.Router == self.router.Id +} + +// lookupTerminatorOwner returns the id of the router that owns terminator id and whether the +// terminator currently exists. A not-found terminator returns ("", false, nil); other read errors +// are returned so the caller can default to keeping the id rather than acting on incomplete state. +func (self *baseHandler) lookupTerminatorOwner(id string) (routerId string, present bool, err error) { + terminator, err := self.network.Terminator.Read(id) + if err != nil { + if boltz.IsErrNotFoundErr(err) { + return "", false, nil + } + return "", false, err + } + return terminator.Router, true, nil +} + +// selectOwnedTerminators returns the terminator ids from a remove request that this router may +// remove, dropping (and logging) any whose terminator is owned by a different router, so a router +// can only remove terminators it owns. Absent ids are kept so a delete racing a not-yet-applied +// create is still ordered after it. +func (self *baseHandler) selectOwnedTerminators(ids []string) []string { + kept, rejected := filterOwnedTerminators(ids, self.router.Id, self.lookupTerminatorOwner) + if rejected > 0 { + pfxlog.Logger(). + WithField("routerId", self.router.Id). + WithField("rejected", rejected). + Warn("router attempted to remove terminators it does not own; rejected") + } + return kept +} + +// filterOwnedTerminators returns the ids owned by requestingRouterId (or not currently present), +// dropping ids whose terminator resolves to a different router; rejected counts those drops. A +// lookup error keeps the id, so an unresolved owner is not treated as an ownership violation. +func filterOwnedTerminators(ids []string, requestingRouterId string, lookup func(string) (routerId string, present bool, err error)) (kept []string, rejected int) { + kept = make([]string, 0, len(ids)) + for _, id := range ids { + routerId, present, err := lookup(id) + if err == nil && present && routerId != requestingRouterId { + rejected++ + continue + } + kept = append(kept, id) + } + return kept, rejected +} diff --git a/controller/handler_ctrl/base_test.go b/controller/handler_ctrl/base_test.go new file mode 100644 index 000000000..43fed9c9f --- /dev/null +++ b/controller/handler_ctrl/base_test.go @@ -0,0 +1,92 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package handler_ctrl + +import ( + "errors" + "testing" + + "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/models" + "github.com/stretchr/testify/require" +) + +func Test_filterOwnedTerminators(t *testing.T) { + const me = "router-me" + + // mine: present, owned by the requesting router; other: present, owned by a different router; + // gone: not present; err: lookup fails. + lookup := func(id string) (string, bool, error) { + switch id { + case "mine": + return me, true, nil + case "other": + return "router-other", true, nil + case "err": + return "", false, errors.New("boom") + default: // "gone" and anything else + return "", false, nil + } + } + + t.Run("keeps owned and absent ids", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"mine", "gone"}, me, lookup) + req.Equal([]string{"mine", "gone"}, kept) + req.Zero(rejected) + }) + + t.Run("rejects ids owned by another router", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"other", "mine"}, me, lookup) + req.Equal([]string{"mine"}, kept, "a router may only remove terminators it owns") + req.Equal(1, rejected) + }) + + t.Run("a lookup error keeps the id", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"err"}, me, lookup) + req.Equal([]string{"err"}, kept, "an unresolved owner must not be treated as an ownership violation") + req.Zero(rejected) + }) + + t.Run("mixes kept and rejected", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"mine", "other", "gone", "err"}, me, lookup) + req.Equal([]string{"mine", "gone", "err"}, kept) + req.Equal(1, rejected) + }) +} + +// Test_ownsTerminator covers the single-terminator check used by the remove and update handlers, +// which reject outright rather than filtering. +func Test_ownsTerminator(t *testing.T) { + handler := &baseHandler{router: &model.Router{BaseEntity: models.BaseEntity{Id: "router-me"}}} + + t.Run("owned by the requesting router", func(t *testing.T) { + require.True(t, handler.ownsTerminator(&model.Terminator{Router: "router-me"})) + }) + + t.Run("owned by a different router", func(t *testing.T) { + require.False(t, handler.ownsTerminator(&model.Terminator{Router: "router-other"})) + }) + + t.Run("unset owner", func(t *testing.T) { + require.False(t, handler.ownsTerminator(&model.Terminator{}), + "a terminator with no owner must not be treated as owned by the requester") + }) +} diff --git a/controller/handler_ctrl/connect.go b/controller/handler_ctrl/connect.go index 21e6aa32c..22f3d782c 100644 --- a/controller/handler_ctrl/connect.go +++ b/controller/handler_ctrl/connect.go @@ -19,31 +19,94 @@ package handler_ctrl import ( "crypto/sha1" "crypto/x509" - "errors" "fmt" "time" "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" - "github.com/openziti/foundation/v2/stringz" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/cert" + "github.com/openziti/ziti/v2/controller/model" "github.com/openziti/ziti/v2/controller/network" ) type ConnectHandler struct { identity identity.Identity network *network.Network + + // signingCertRoots holds the edge enrollment signing CA bundle. A router presents its enrollment + // certificate as its control channel client certificate, so a deployment whose signing CA sits + // outside the controller's own trust bundle would otherwise have every router refused here. + signingCertRoots []*x509.Certificate + + // separatelyValidatedTypes holds the control-channel type headers that are dispatched to a + // separate, self-validating acceptor (currently the raft mesh, when clustering is enabled). + separatelyValidatedTypes map[string]struct{} } -func NewConnectHandler(identity identity.Identity, network *network.Network) *ConnectHandler { +// NewConnectHandler returns a ConnectHandler that admits routers whose leaf certificate chains either to +// the controller's own CA bundle or to signingCertRoots, the edge enrollment signing CA bundle. +// signingCertRoots may be empty, in which case only the controller's bundle is trusted. +func NewConnectHandler(identity identity.Identity, network *network.Network, signingCertRoots []*x509.Certificate) *ConnectHandler { return &ConnectHandler{ - identity: identity, - network: network, + identity: identity, + network: network, + signingCertRoots: signingCertRoots, } } +// SetSeparatelyValidatedChannelTypes records the control-channel type headers that are dispatched to a +// separate, self-validating acceptor (e.g. the raft mesh). Connections carrying one of these types are +// skipped by HandleConnection; everything else - router control channel types, unrecognized types, and +// legacy (no type header) connections, all of which the dispatcher routes to the router control +// acceptor - is validated here. This must be populated before the listener begins accepting. +func (self *ConnectHandler) SetSeparatelyValidatedChannelTypes(types map[string]struct{}) { + self.separatelyValidatedTypes = types +} + +// isSeparatelyValidated reports whether the connection's channel type is handled by a separate, +// self-validating acceptor and therefore must not be validated as a router control connection here. +func (self *ConnectHandler) isSeparatelyValidated(hello *channel.Hello) bool { + underlayType, found := hello.Headers[channel.TypeHeader] + if !found { + return false + } + _, ok := self.separatelyValidatedTypes[string(underlayType)] + return ok +} + +// isFirstCtrlConnection reports whether this hello establishes a new channel rather than adding an +// underlay to an existing grouped channel. A legacy (non-grouped) dial is always a new channel; for a +// grouped dial only the connection carrying IsFirstGroupConnection is. +func isFirstCtrlConnection(hello *channel.Hello) bool { + headers := channel.Headers(hello.Headers) + if grouped, _ := headers.GetBoolHeader(channel.IsGroupedHeader); !grouped { + return true + } + first, _ := headers.GetBoolHeader(channel.IsFirstGroupConnection) + return first +} + +// withinChurnLimit reports whether an established connection is too new to be displaced by a new one. +// +// This is admission policy, not the uniqueness guarantee. At most one connection per router is enforced +// under the per-router lock in Network.ConnectRouter; this runs against the connected map with no lock +// held, so it can only avoid paying for a bind that would be refused there anyway. +// +// Displacing an established connection costs a round trip: the occupant's teardown runs, the connect is +// refused, and the router redials into the freed slot. A connection that has only just been established +// is therefore protected for churnLimit, so a flapping router cannot thrash a working channel. A zero +// limit disables the protection, making every new connection able to displace the current one. +func withinChurnLimit(connected *model.Router, churnLimit time.Duration) bool { + return time.Since(connected.ConnectTime) < churnLimit +} + func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates []*x509.Certificate) error { - if _, found := hello.Headers[channel.TypeHeader]; found { + // Connections whose channel type is handled by a separate, self-validating acceptor (e.g. the raft + // mesh) are validated there, so skip them. Everything else - router control channel types, + // unrecognized types, and legacy (no type header) connections - is dispatched to the router control + // acceptor and must be validated here. + if self.isSeparatelyValidated(hello) { return nil } @@ -51,63 +114,43 @@ func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates log := pfxlog.Logger().WithField("routerId", id) - // verify cert chain if len(certificates) == 0 { return fmt.Errorf("no certificates provided, unable to verify dialer, routerId: %v", id) } - config := self.identity.ServerTLSConfig() - - opts := x509.VerifyOptions{ - Roots: config.RootCAs, - Intermediates: x509.NewCertPool(), - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + // Verify the peer's leaf certificate (certificates[0], the certificate whose private key the TLS + // handshake proved) chains to the controller CA or the edge signing CA, and bind the router + // fingerprint check to that verified leaf. Matching the enrolled fingerprint against any presented + // certificate would let a peer present its own leaf followed by a target router's public + // certificate and pass without that router's private key. + leaf, err := cert.VerifyLeafCertChain(self.identity.CA(), certificates, self.signingCertRoots...) + if err != nil { + return fmt.Errorf("unable to verify dialer, routerId: %v: %w", id, err) } + fingerprint := fmt.Sprintf("%x", sha1.Sum(leaf.Raw)) + log.Debugf("peer leaf certificate fingerprint [%s], common name [%s]", fingerprint, leaf.Subject.CommonName) - var validFingerPrints []string - var errorList []error - - for _, cert := range certificates { - if cert.IsCA { - opts.Intermediates.AddCert(cert) - } - } - - for i, cert := range certificates { - if !cert.IsCA { - if _, err := cert.Verify(opts); err == nil { - fingerprint := fmt.Sprintf("%x", sha1.Sum(cert.Raw)) - validFingerPrints = append(validFingerPrints, fingerprint) - log.Debugf("%d): peer certificate fingerprint [%s]", i, fingerprint) - log.Debugf("%d): peer common name [%s]", i, cert.Subject.CommonName) - } else { - errorList = append(errorList, err) + // The churn / already-connected guard applies only when establishing a new channel. Additional + // underlays of an existing grouped control channel legitimately arrive while the router is already + // connected and must not be rejected here. + if isFirstCtrlConnection(hello) { + if router := self.network.GetConnectedRouter(id); router != nil { + if withinChurnLimit(router, self.network.GetOptions().RouterConnectChurnLimit) { + log.WithField("routerName", router.Name).Error("router already connected and churn threshold not met") + return fmt.Errorf("router already connected id: %s, name: %s", id, router.Name) } + log.WithField("routerName", router.Name).Warn("router already connected, but churn threshold met. replacing connection") } } - if len(validFingerPrints) == 0 && len(errorList) > 0 { - return errors.Join(errorList...) - } - - log.Debugf("peer has [%d] valid certificates out of [%v] submitted", len(validFingerPrints), len(certificates)) - - if router := self.network.GetConnectedRouter(id); router != nil { - if time.Since(router.ConnectTime) < self.network.GetOptions().RouterConnectChurnLimit { - log.WithField("routerName", router.Name).Error("router already connected and churn threshold not met") - return fmt.Errorf("router already connected id: %s, name: %s", id, router.Name) - } - log.WithField("routerName", router.Name).Warn("router already connected, but churn threshold met. replacing connection") - } - if r, err := self.network.GetRouter(id); err == nil { if r.Fingerprint == nil { log.Error("router enrollment incomplete") return fmt.Errorf("router enrollment incomplete, routerId: %v", id) } - if !stringz.Contains(validFingerPrints, *r.Fingerprint) { - log.WithField("fp", *r.Fingerprint).WithField("givenFps", validFingerPrints).Error("router fingerprint mismatch") - return fmt.Errorf("incorrect fingerprint/unenrolled router, routerId: %v, given fingerprints: %v", id, validFingerPrints) + if fingerprint != *r.Fingerprint { + log.WithField("fp", *r.Fingerprint).WithField("givenFp", fingerprint).Error("router fingerprint mismatch") + return fmt.Errorf("incorrect fingerprint/unenrolled router, routerId: %v, given fingerprint: %v", id, fingerprint) } if r.Disabled { log.Error("router disabled") diff --git a/controller/handler_ctrl/connect_test.go b/controller/handler_ctrl/connect_test.go new file mode 100644 index 000000000..d4636f07b --- /dev/null +++ b/controller/handler_ctrl/connect_test.go @@ -0,0 +1,214 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package handler_ctrl + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// meshChannelType mirrors controller/raft/mesh.ChannelTypeMesh without importing that package. +const meshChannelType = "ctrl.mesh" + +func helloWithType(chType string) *channel.Hello { + h := channel.Headers{} + if chType != "" { + h.PutStringHeader(channel.TypeHeader, chType) + } + return &channel.Hello{Headers: h} +} + +// Test_ConnectHandler_isSeparatelyValidated covers the rule that only channel types dispatched to a +// dedicated, self-validating acceptor are skipped; every other type - including unrecognized types and +// the mesh type on a non-clustered controller - routes to the router control acceptor and must be +// validated here. +func Test_ConnectHandler_isSeparatelyValidated(t *testing.T) { + // Clustered controller: only the mesh type has a dedicated acceptor. + clustered := &ConnectHandler{separatelyValidatedTypes: map[string]struct{}{meshChannelType: {}}} + require.True(t, clustered.isSeparatelyValidated(helloWithType(meshChannelType)), "mesh is validated by its own acceptor") + require.False(t, clustered.isSeparatelyValidated(helloWithType(ctrlchan.ChannelTypeDefault)), "router ctrl type is validated here") + require.False(t, clustered.isSeparatelyValidated(helloWithType(ctrlchan.ChannelTypeHighPriority))) + require.False(t, clustered.isSeparatelyValidated(helloWithType("bogus")), "unrecognized types route to the router acceptor and must be validated") + require.False(t, clustered.isSeparatelyValidated(helloWithType("")), "legacy (no type) connections are validated here") + + // Non-clustered controller: no dedicated acceptors, so even a mesh-typed connection routes to the + // router acceptor and must be validated. + standalone := &ConnectHandler{separatelyValidatedTypes: map[string]struct{}{}} + require.False(t, standalone.isSeparatelyValidated(helloWithType(meshChannelType)), "mesh on a non-clustered controller must be validated") + require.False(t, standalone.isSeparatelyValidated(helloWithType(ctrlchan.ChannelTypeDefault))) +} + +func Test_isFirstCtrlConnection(t *testing.T) { + // Legacy / non-grouped dial: no grouped header -> treated as a new channel. + require.True(t, isFirstCtrlConnection(&channel.Hello{Headers: channel.Headers{}})) + + grouped := channel.Headers{} + grouped.PutBoolHeader(channel.IsGroupedHeader, true) + grouped.PutBoolHeader(channel.IsFirstGroupConnection, true) + require.True(t, isFirstCtrlConnection(&channel.Hello{Headers: grouped}), "grouped first connection") + + additional := channel.Headers{} + additional.PutBoolHeader(channel.IsGroupedHeader, true) + require.False(t, isFirstCtrlConnection(&channel.Hello{Headers: additional}), "additional underlay (no first flag)") + + notFirst := channel.Headers{} + notFirst.PutBoolHeader(channel.IsGroupedHeader, true) + notFirst.PutBoolHeader(channel.IsFirstGroupConnection, false) + require.False(t, isFirstCtrlConnection(&channel.Hello{Headers: notFirst}), "additional underlay (first=false)") +} + +// caPoolIdentity is a minimal identity.Identity whose only useful method is CA. The certificate +// verification path of HandleConnection consults nothing else, so the remaining interface methods are +// left to the embedded nil interface (never called on the paths exercised here). +type caPoolIdentity struct { + identity.Identity + roots *x509.CertPool +} + +func (f *caPoolIdentity) CA() *x509.CertPool { return f.roots } + +type ctCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var ctSerial int64 + +func ctMkCert(t *testing.T, cn string, isCA bool, signer *ctCertAndKey) *ctCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + ctSerial++ + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(ctSerial), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + } + if isCA { + tmpl.IsCA = true + tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + } else { + tmpl.KeyUsage = x509.KeyUsageDigitalSignature + tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &ctCertAndKey{cert: c, key: key} +} + +// Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf covers the router control-channel +// verification: a connection dispatched to the router acceptor must present a leaf that chains to the +// controller CA. In particular, presenting a self-signed leaf followed by a legitimate router's public +// certificate as filler must be rejected - the check is bound to the leaf, not to "some presented cert +// chains". These paths fail during certificate verification, before any network state is consulted. +func Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf(t *testing.T) { + req := require.New(t) + + root := ctMkCert(t, "root", true, nil) + inter := ctMkCert(t, "int", true, root) + roots := x509.NewCertPool() + roots.AddCert(root.cert) + roots.AddCert(inter.cert) + + handler := &ConnectHandler{ + identity: &caPoolIdentity{roots: roots}, + separatelyValidatedTypes: map[string]struct{}{}, + } + + // A legitimate, CA-chained certificate an attacker could scrape off the wire and present as filler. + legit := ctMkCert(t, "legit-router", false, inter) + // The attacker's own self-signed leaf - it does not chain to the CA. + forged := ctMkCert(t, "forged", false, nil) + + hello := &channel.Hello{IdToken: "router1", Headers: channel.Headers{}} + + req.Error(handler.HandleConnection(hello, nil), "no certificates must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert}), + "self-signed leaf that does not chain to the CA must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert, legit.cert}), + "self-signed leaf backed by a scraped CA-chained filler cert must be rejected") +} + +// Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated verifies that connections whose type is +// handled by a separate acceptor (e.g. the raft mesh on a clustered controller) are not validated here, +// even when the presented certificate would fail the router control-channel check. +func Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated(t *testing.T) { + req := require.New(t) + + forged := ctMkCert(t, "forged", false, nil) + handler := &ConnectHandler{ + separatelyValidatedTypes: map[string]struct{}{meshChannelType: {}}, + } + + req.NoError(handler.HandleConnection(helloWithType(meshChannelType), []*x509.Certificate{forged.cert}), + "mesh-typed connection is validated by its own acceptor and must be skipped here") +} + +// Test_withinChurnLimit pins the admission policy that decides whether an already-connected router's +// channel may be displaced by a new connection. +// +// It is the only thing rate-limiting displacement. Network.ConnectRouter always displaces an occupant it +// does not recognise, so without this a spurious first-connection hello would tear down a healthy control +// channel and force the router to redial. Uniqueness itself is guaranteed under the per-router lock in +// ConnectRouter, not here, so this check exists purely to protect a working connection from churn. +func Test_withinChurnLimit(t *testing.T) { + connectedAt := func(d time.Duration) *model.Router { + r := &model.Router{ConnectTime: time.Now().Add(-d)} + r.Id = "r1" + return r + } + + tests := []struct { + name string + since time.Duration + churnLimit time.Duration + protected bool + }{ + {"a connection just established is protected", 0, time.Minute, true}, + {"still protected part way through the window", 30 * time.Second, time.Minute, true}, + {"displaceable once the window has passed", 2 * time.Minute, time.Minute, false}, + // A zero limit is a supported setting and means "always allow takeover", which is what the option + // existed to make configurable in the first place. + {"a zero limit protects nothing", 0, 0, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.protected, withinChurnLimit(connectedAt(tt.since), tt.churnLimit)) + }) + } +} diff --git a/controller/handler_ctrl/remove_terminator.go b/controller/handler_ctrl/remove_terminator.go index 18b9b9e81..39a34c2c9 100644 --- a/controller/handler_ctrl/remove_terminator.go +++ b/controller/handler_ctrl/remove_terminator.go @@ -64,6 +64,16 @@ func (self *removeTerminatorHandler) handleRemoveTerminator(msg *channel.Message return } + if !self.ownsTerminator(terminator) { + log. + WithField("routerId", self.router.Id). + WithField("terminator", request.TerminatorId). + WithField("terminatorRouterId", terminator.Router). + Warn("router attempted to remove a terminator it does not own; rejected") + handler_common.SendFailure(msg, ch, "terminator not owned by requesting router") + return + } + if err := self.network.Terminator.Delete(request.TerminatorId, self.newChangeContext(ch, "fabric.remove.terminator")); err == nil { log. WithField("routerId", ch.Id()). diff --git a/controller/handler_ctrl/remove_terminators.go b/controller/handler_ctrl/remove_terminators.go index f9e324d5e..15d0c4899 100644 --- a/controller/handler_ctrl/remove_terminators.go +++ b/controller/handler_ctrl/remove_terminators.go @@ -59,23 +59,30 @@ func (self *removeTerminatorsHandler) HandleReceive(msg *channel.Message, ch cha func (self *removeTerminatorsHandler) handleRemoveTerminators(msg *channel.Message, ch channel.Channel, request *ctrl_pb.RemoveTerminatorsRequest) { log := pfxlog.ContextLogger(ch.Label()) - // Don't pre-filter by IsEntityPresent here. The create for a terminator may be - // in-flight in raft but not yet applied to the DB. If we skip it here, the create - // will apply after we return success, leaving an orphan. By sending all IDs through - // raft, the delete will be ordered after the create and ApplyDeleteBatch will handle - // non-existent IDs gracefully. if len(request.TerminatorIds) == 0 { handler_common.SendSuccess(msg, ch, "") return } - if err := self.network.Terminator.DeleteBatch(request.TerminatorIds, self.newChangeContext(ch, "fabric.remove.terminators.batch")); err == nil { + // Drop ids this router doesn't own, so it can't remove another router's terminators. Absent ids + // are kept (not pre-filtered by presence): the create for a terminator may be in-flight in raft + // but not yet applied to the DB, so sending it through raft orders the delete after the create, + // and ApplyDeleteBatch handles non-existent ids gracefully. + toDelete := self.selectOwnedTerminators(request.TerminatorIds) + if len(toDelete) == 0 { + handler_common.SendSuccess(msg, ch, "") + return + } + + if err := self.network.Terminator.DeleteBatch(toDelete, self.newChangeContext(ch, "fabric.remove.terminators.batch")); err == nil { log. WithField("routerId", ch.Id()). - WithField("terminatorIds", request.TerminatorIds). + WithField("terminatorIds", toDelete). Info("removed terminators") handler_common.SendSuccess(msg, ch, "") - } else if command.WasRateLimited(err) { + } else if command.WasRateLimited(err) || command.WasLeaderless(err) { + // A leaderless cluster (during a membership change) is transient; signal busy so the router retries + // rather than treating the removal as a permanent failure. handler_common.SendServerBusy(msg, ch, "remove.terminators") } else { handler_common.SendFailure(msg, ch, err.Error()) diff --git a/controller/handler_ctrl/update_terminator.go b/controller/handler_ctrl/update_terminator.go index d4a87392b..ef132d55b 100644 --- a/controller/handler_ctrl/update_terminator.go +++ b/controller/handler_ctrl/update_terminator.go @@ -66,6 +66,16 @@ func (self *updateTerminatorHandler) handleUpdateTerminator(msg *channel.Message return } + if !self.ownsTerminator(terminator) { + log. + WithField("routerId", self.router.Id). + WithField("terminator", request.TerminatorId). + WithField("terminatorRouterId", terminator.Router). + Warn("router attempted to update a terminator it does not own; rejected") + handler_common.SendFailure(msg, ch, "terminator not owned by requesting router") + return + } + if !request.UpdateCost && !request.UpdatePrecedence { // nothing to do handler_common.SendSuccess(msg, ch, "") diff --git a/controller/handler_edge_ctrl/common.go b/controller/handler_edge_ctrl/common.go index 50176054c..e3f3e75de 100644 --- a/controller/handler_edge_ctrl/common.go +++ b/controller/handler_edge_ctrl/common.go @@ -11,7 +11,6 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" "github.com/openziti/sdk-golang/ziti/edge" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/common/logcontext" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" @@ -23,6 +22,7 @@ import ( "github.com/openziti/ziti/v2/controller/models" "github.com/openziti/ziti/v2/controller/network" "github.com/openziti/ziti/v2/controller/oidc_auth" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/xt" "github.com/sirupsen/logrus" ) @@ -314,10 +314,49 @@ func (self *baseSessionRequestContext) loadFromBolt(sessionToken string, apiSess return } + if !strings.HasPrefix(sessionToken, oidc_auth.JwtTokenPrefix) { + self.session, err = self.handler.getAppEnv().Managers.Session.ReadByToken(sessionToken) + + if err != nil { + if boltz.IsErrNotFoundErr(err) { + self.err = InvalidSessionError{} + } else { + self.err = internalError(err) + } + logrus. + WithField("operation", self.handler.Label()). + WithError(self.err).Errorf("invalid session") + return + } + + if self.session.ApiSessionId != self.apiSession.Id { + self.err = InvalidSessionError{} + logrus. + WithField("operation", self.handler.Label()). + WithField("sessionId", self.session.Id). + WithField("sessionApiSessionId", self.session.ApiSessionId). + WithField("apiSessionId", self.apiSession.Id). + WithError(self.err).Error("session does not belong to api session") + } + return + } + serviceAccessClaims, err := self.env.ValidateServiceAccessToken(sessionToken, &self.apiSession.Id) if err != nil { - self.err = internalError(err) + // A token-level failure (bad/expired/mismatched/revoked token) means the client should discard + // and re-create its session, so return InvalidSession. An infrastructure failure (e.g. a + // revocation datastore read) must stay an internalError, otherwise a transient controller fault + // would make clients discard valid sessions and trigger a reauthentication storm. + var invalidToken *common.InvalidTokenError + if errors.As(err, &invalidToken) { + self.err = InvalidSessionError{} + logrus. + WithField("operation", self.handler.Label()). + WithError(err).Error("service access token invalid; treating as invalid session") + } else { + self.err = internalError(err) + } return } diff --git a/controller/handler_edge_ctrl/common_test.go b/controller/handler_edge_ctrl/common_test.go new file mode 100644 index 000000000..035bad4bd --- /dev/null +++ b/controller/handler_edge_ctrl/common_test.go @@ -0,0 +1,156 @@ +package handler_edge_ctrl + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/controller/change" + "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/env" + "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/network" + "github.com/stretchr/testify/require" +) + +type legacySessionTestHandler struct { + appEnv *env.AppEnv +} + +func (self *legacySessionTestHandler) getAppEnv() *env.AppEnv { + return self.appEnv +} + +func (*legacySessionTestHandler) getNetwork() *network.Network { + return nil +} + +func (*legacySessionTestHandler) getChannel() channel.Channel { + return nil +} + +func (*legacySessionTestHandler) Label() string { + return "legacy-session-test" +} + +func newLegacySessionTestEntities(t *testing.T, testCtx *model.TestContext) (*model.Managers, *model.Identity, *model.EdgeService) { + managers := testCtx.GetManagers() + identity := &model.Identity{ + Name: uuid.NewString(), + IdentityTypeId: db.DefaultIdentityType, + } + require.NoError(t, managers.Identity.Create(identity, change.New())) + + service := &model.EdgeService{Name: uuid.NewString()} + require.NoError(t, managers.EdgeService.Create(service, change.New())) + + edgeRouter := &model.EdgeRouter{Name: uuid.NewString()} + require.NoError(t, managers.EdgeRouter.Create(edgeRouter, change.New())) + + servicePolicy := &model.ServicePolicy{ + Name: uuid.NewString(), + Semantic: db.SemanticAllOf, + IdentityRoles: []string{"#all"}, + ServiceRoles: []string{"#all"}, + PolicyType: db.PolicyTypeDialName, + } + require.NoError(t, managers.ServicePolicy.Create(servicePolicy, change.New())) + + edgeRouterPolicy := &model.EdgeRouterPolicy{ + Name: uuid.NewString(), + Semantic: db.SemanticAllOf, + IdentityRoles: []string{"#all"}, + EdgeRouterRoles: []string{"#all"}, + } + require.NoError(t, managers.EdgeRouterPolicy.Create(edgeRouterPolicy, change.New())) + + serviceEdgeRouterPolicy := &model.ServiceEdgeRouterPolicy{ + Name: uuid.NewString(), + Semantic: db.SemanticAllOf, + ServiceRoles: []string{"#all"}, + EdgeRouterRoles: []string{"#all"}, + } + require.NoError(t, managers.ServiceEdgeRouterPolicy.Create(serviceEdgeRouterPolicy, change.New())) + + return managers, identity, service +} + +func TestLoadFromBoltSupportsOpaqueServiceSessionTokens(t *testing.T) { + testCtx := model.NewTestContext(t) + defer testCtx.Cleanup() + testCtx.Init() + + managers, identity, service := newLegacySessionTestEntities(t, testCtx) + + apiSession := &model.ApiSession{ + Token: uuid.NewString(), + IdentityId: identity.Id, + Identity: identity, + LastActivityAt: time.Now(), + } + _, err := managers.ApiSession.Create(nil, apiSession, nil) + require.NoError(t, err) + + legacySession := &model.Session{ + Token: uuid.NewString(), + IdentityId: identity.Id, + ApiSessionId: apiSession.Id, + ServiceId: service.Id, + Type: db.SessionTypeDial, + } + _, err = managers.Session.Create(legacySession, change.New()) + require.NoError(t, err) + + handler := &legacySessionTestHandler{ + appEnv: &env.AppEnv{Managers: managers}, + } + requestCtx := &baseSessionRequestContext{handler: handler} + + requestCtx.loadFromBolt(legacySession.Token, apiSession.Token) + + require.NoError(t, requestCtx.err) + require.Equal(t, legacySession.Id, requestCtx.session.Id) + require.Equal(t, apiSession.Id, requestCtx.apiSession.Id) +} + +func TestLoadFromBoltRejectsOpaqueServiceSessionForDifferentApiSession(t *testing.T) { + testCtx := model.NewTestContext(t) + defer testCtx.Cleanup() + testCtx.Init() + + managers, identity, service := newLegacySessionTestEntities(t, testCtx) + + newApiSession := func() *model.ApiSession { + apiSession := &model.ApiSession{ + Token: uuid.NewString(), + IdentityId: identity.Id, + Identity: identity, + LastActivityAt: time.Now(), + } + _, err := managers.ApiSession.Create(nil, apiSession, nil) + require.NoError(t, err) + return apiSession + } + + ownerApiSession := newApiSession() + otherApiSession := newApiSession() + legacySession := &model.Session{ + Token: uuid.NewString(), + IdentityId: identity.Id, + ApiSessionId: ownerApiSession.Id, + ServiceId: service.Id, + Type: db.SessionTypeDial, + } + _, err := managers.Session.Create(legacySession, change.New()) + require.NoError(t, err) + + handler := &legacySessionTestHandler{ + appEnv: &env.AppEnv{Managers: managers}, + } + requestCtx := &baseSessionRequestContext{handler: handler} + + requestCtx.loadFromBolt(legacySession.Token, otherApiSession.Token) + + require.IsType(t, InvalidSessionError{}, requestCtx.err) +} diff --git a/controller/handler_edge_ctrl/create_circuit.go b/controller/handler_edge_ctrl/create_circuit.go index 10cd999d9..03a60f09c 100644 --- a/controller/handler_edge_ctrl/create_circuit.go +++ b/controller/handler_edge_ctrl/create_circuit.go @@ -58,7 +58,7 @@ func (self *createCircuitHandler) HandleReceiveCreateCircuitV1(msg *channel.Mess } ctx := &CreateCircuitRequestContext{ - baseSessionRequestContext: baseSessionRequestContext{handler: self, msg: msg}, + baseSessionRequestContext: baseSessionRequestContext{handler: self, msg: msg, env: self.appEnv}, req: req, } diff --git a/controller/handler_edge_ctrl/create_circuit_v3.go b/controller/handler_edge_ctrl/create_circuit_v3.go index dd81e3e4e..4e81e89b3 100644 --- a/controller/handler_edge_ctrl/create_circuit_v3.go +++ b/controller/handler_edge_ctrl/create_circuit_v3.go @@ -23,20 +23,22 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" "github.com/openziti/sdk-golang/ziti/edge" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/ctrl_msg" "github.com/openziti/ziti/v2/common/logcontext" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" "github.com/openziti/ziti/v2/controller/env" "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/xt" "github.com/sirupsen/logrus" ) // NewCreateCircuitV3Handler creates a handler for CreateCircuitV3 requests. These requests // come from routers that have already authorized the dial locally via RDM, so no service -// session token is required. Instead, the request carries identity ID, service ID, and -// a pre-assigned circuit ID. +// session token is required. An API session token is still required: it proves the dial is +// being made on behalf of an authenticated identity, and the controller uses its claims, +// not the request's identity ID, as the authoritative dialing identity. The request also +// carries the service ID and a pre-assigned circuit ID. func NewCreateCircuitV3Handler(appEnv *env.AppEnv, ch channel.Channel) channel.TypedReceiveHandler { handler := &createCircuitHandler{ baseRequestHandler: baseRequestHandler{ @@ -80,6 +82,7 @@ func (self *createCircuitHandler) createCircuitV3(ctx *createCircuitV3RequestCon if !ctx.loadRouter() { return } + ctx.validateApiSession() ctx.setupLogContext() ctx.loadServiceByIdForDial() ctx.verifyEdgeRouterAccessForIdentity() @@ -96,7 +99,7 @@ func (self *createCircuitHandler) createCircuitV3(ctx *createCircuitV3RequestCon } log := pfxlog.ContextLogger(self.ch.Label()). - WithField("identityId", ctx.req.IdentityId). + WithField("identityId", ctx.identityId()). WithField("serviceId", ctx.req.ServiceId). WithField("circuitId", circuitInfo.Id) @@ -124,17 +127,69 @@ func (self *createCircuitV3RequestContext) UpdateResponse(m *channel.Message) { } } +// validateApiSession validates the API session token accompanying the request and +// establishes the dialing identity from its claims. Connect-v2 routers authorize the dial +// locally against their data model, but the identity ID in the request is router-supplied, +// so the controller validates the token itself. This both proves the dial is being made on +// behalf of an authenticated identity and re-checks expiration and revocation, which the +// router may not have seen yet. +func (self *createCircuitV3RequestContext) validateApiSession() { + if self.err != nil { + return + } + + log := logrus.WithField("routerId", self.sourceRouter.Id). + WithField("operation", self.handler.Label()). + WithField("requestedIdentityId", self.req.IdentityId). + WithField("serviceId", self.req.ServiceId) + + if self.req.ApiSessionToken == "" { + self.err = InvalidApiSessionError{} + log.Error("no api session token provided in create circuit v3 request") + return + } + + claims, err := self.env.ValidateAccessToken(self.req.ApiSessionToken) + if err != nil { + self.err = InvalidApiSessionError{} + log.WithError(err).Error("invalid api session token in create circuit v3 request") + return + } + + // The token is authoritative for identity. A mismatch means the router asked for a + // circuit on behalf of an identity other than the one that authenticated. + if claims.Subject != self.req.IdentityId { + self.err = InvalidApiSessionError{} + log.WithField("apiSessionIdentityId", claims.Subject). + WithField("apiSessionId", claims.ApiSessionId). + Error("create circuit v3 request identity does not match api session identity") + return + } + + self.accessClaims = claims +} + +// identityId returns the identity the circuit is being created for, taken from the +// validated API session token claims. Only valid once validateApiSession has succeeded. +func (self *createCircuitV3RequestContext) identityId() string { + if self.accessClaims == nil { + return "" + } + return self.accessClaims.Subject +} + func (self *createCircuitV3RequestContext) setupLogContext() { if self.err != nil { return } self.logContext = logcontext.NewContext() - traceSpec := self.handler.getAppEnv().TraceManager.GetIdentityTrace(self.req.IdentityId) + traceSpec := self.handler.getAppEnv().TraceManager.GetIdentityTrace(self.identityId()) if traceSpec != nil && time.Now().Before(traceSpec.Until) { self.logContext.SetChannelsMask(traceSpec.ChannelMask) self.logContext.WithField("traceId", traceSpec.TraceId) } + self.logContext.WithField("apiSessionId", self.accessClaims.ApiSessionId) } func (self *createCircuitV3RequestContext) loadServiceByIdForDial() { @@ -157,11 +212,11 @@ func (self *createCircuitV3RequestContext) loadServiceByIdForDial() { return } - dialable, err := self.handler.getAppEnv().Managers.EdgeService.IsDialableByIdentity(self.req.ServiceId, self.req.IdentityId) + dialable, err := self.handler.getAppEnv().Managers.EdgeService.IsDialableByIdentity(self.req.ServiceId, self.identityId()) if err != nil { self.err = internalError(err) logrus.WithField("serviceId", self.req.ServiceId). - WithField("identityId", self.req.IdentityId). + WithField("identityId", self.identityId()). WithField("operation", self.handler.Label()). WithError(err). Error("unable to verify dial access to service") @@ -171,7 +226,7 @@ func (self *createCircuitV3RequestContext) loadServiceByIdForDial() { if !dialable { self.err = InvalidServiceError{} logrus.WithField("serviceId", self.req.ServiceId). - WithField("identityId", self.req.IdentityId). + WithField("identityId", self.identityId()). WithField("operation", self.handler.Label()). Error("identity does not have dial access to service") } @@ -181,16 +236,16 @@ func (self *createCircuitV3RequestContext) verifyEdgeRouterAccessForIdentity() { if self.err != nil { return } - self.verifyEdgeRouterAccess(self.req.IdentityId, self.service.Id) + self.verifyEdgeRouterAccess(self.identityId(), self.service.Id) } func (self *createCircuitV3RequestContext) newCircuitCreateParms(serviceId string, peerData map[uint32][]byte) model.CreateCircuitParams { return &connectV3CircuitParams{ circuitId: self.req.CircuitId, serviceId: serviceId, - identityId: self.req.IdentityId, + identityId: self.identityId(), sourceRouter: self.sourceRouter, - clientId: &identity.TokenId{Token: self.req.IdentityId, Data: peerData}, + clientId: &identity.TokenId{Token: self.identityId(), Data: peerData}, logCtx: self.logContext, deadline: time.Now().Add(self.handler.getAppEnv().GetHostController().GetNetwork().GetOptions().RouteTimeout), } diff --git a/controller/handler_edge_ctrl/create_terminator_v2.go b/controller/handler_edge_ctrl/create_terminator_v2.go index 3499642a3..33e9944e0 100644 --- a/controller/handler_edge_ctrl/create_terminator_v2.go +++ b/controller/handler_edge_ctrl/create_terminator_v2.go @@ -58,11 +58,6 @@ func (self *createTerminatorV2Handler) Label() string { } func (self *createTerminatorV2Handler) HandleReceive(msg *channel.Message, ch channel.Channel) { - if self.appEnv.GetCommandDispatcher().IsLeaderless() { - pfxlog.ContextLogger(ch.Label()).Error("cluster has no leader, unable to handle create terminator request") - return - } - req := &edge_ctrl_pb.CreateTerminatorV2Request{} if err := proto.Unmarshal(msg.Body, req); err != nil { pfxlog.ContextLogger(ch.Label()).WithError(err).Error("could not unmarshal CreateTerminatorV2Request") @@ -122,6 +117,12 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV }, ctx.newChangeContext()) if err != nil { + // A rate-limited or leaderless dispatch is transient; reply busy so the router requeues + // promptly instead of treating it as a hard failure. + if command.WasRateLimited(err) || command.WasLeaderless(err) { + self.returnError(ctx, busyError(err), logger) + return + } self.returnError(ctx, internalError(err), logger) return } @@ -160,7 +161,7 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV return } } else { - if command.WasRateLimited(err) { + if command.WasRateLimited(err) || command.WasLeaderless(err) { self.returnError(ctx, busyError(err), logger) return } diff --git a/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go b/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go index bfaf312f6..10e9e4933 100644 --- a/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go +++ b/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go @@ -148,7 +148,9 @@ func (self *createTunnelTerminatorV2Handler) CreateTerminator(ctx *createTunnelT return } } else { - if command.WasRateLimited(err) { + // A rate-limited or leaderless dispatch is transient; reply busy so the router requeues + // promptly instead of treating it as a hard failure. + if command.WasRateLimited(err) || command.WasLeaderless(err) { self.returnError(ctx, busyError(err), logger) return } diff --git a/controller/internal/policy/service_policy_enforcer.go b/controller/internal/policy/service_policy_enforcer.go index 64d02f676..c01ff49ce 100644 --- a/controller/internal/policy/service_policy_enforcer.go +++ b/controller/internal/policy/service_policy_enforcer.go @@ -153,7 +153,7 @@ func (enforcer *ServicePolicyEnforcer) Run() error { if session.Type == db.SessionTypeBind { policyType = db.PolicyTypeBind } - query := fmt.Sprintf(`id = "%v" and not isEmpty(from servicePolicies where type = %v and anyOf(services) = "%v")`, identity.Id, policyType.Id(), session.ServiceId) + query := fmt.Sprintf(`id = "%v" and not isEmpty(from servicePolicies where type = "%v" and anyOf(services) = "%v")`, identity.Id, policyType.String(), session.ServiceId) _, count, err := enforcer.appEnv.GetStores().Identity.QueryIds(tx, query) if err != nil { return err diff --git a/controller/internal/routes/enroll_router.go b/controller/internal/routes/enroll_router.go index d52904924..acd95855b 100644 --- a/controller/internal/routes/enroll_router.go +++ b/controller/internal/routes/enroll_router.go @@ -20,8 +20,6 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" - "fmt" - "io" "net/http" "strings" @@ -280,15 +278,11 @@ func (ro *EnrollRouter) legacyGenericEnrollPemHandler(ae *env.AppEnv, rc *respon return } - body, err := io.ReadAll(rc.Request.Body) - - if err != nil { - rc.RespondWithError(fmt.Errorf("could not read body: %w", err)) - return - } - + // The request body has already been read and buffered into rc.Body by + // CreateRequestContext; reuse that buffer rather than reading rc.Request.Body + // again, which would allocate a second full copy of a pre-auth request body. enrollContext.Data = &model.EnrollmentData{ - ClientCsrPem: body, + ClientCsrPem: rc.Body, } ro.processEnrollContext(ae, rc, enrollContext) diff --git a/controller/internal/routes/identity_router.go b/controller/internal/routes/identity_router.go index 2be4af4b3..f2229c8a4 100644 --- a/controller/internal/routes/identity_router.go +++ b/controller/internal/routes/identity_router.go @@ -327,11 +327,11 @@ func (r *IdentityRouter) listServices(ae *env.AppEnv, rc *response.RequestContex typeFilter := "" if params.PolicyType != nil { if strings.EqualFold(*params.PolicyType, db.PolicyTypeBind.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeBind.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeBind.String()) } if strings.EqualFold(*params.PolicyType, db.PolicyTypeDial.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeDial.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeDial.String()) } } diff --git a/controller/internal/routes/service_router.go b/controller/internal/routes/service_router.go index 32672a7c4..398cfa951 100644 --- a/controller/internal/routes/service_router.go +++ b/controller/internal/routes/service_router.go @@ -386,11 +386,11 @@ func (r *ServiceRouter) listIdentities(ae *env.AppEnv, rc *response.RequestConte typeFilter := "" if params.PolicyType != nil { if strings.EqualFold(*params.PolicyType, db.PolicyTypeBind.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeBind.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeBind.String()) } if strings.EqualFold(*params.PolicyType, db.PolicyTypeDial.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeDial.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeDial.String()) } } diff --git a/controller/internal/routes/session_router.go b/controller/internal/routes/session_router.go index 2227fd624..0a6d0d3a7 100644 --- a/controller/internal/routes/session_router.go +++ b/controller/internal/routes/session_router.go @@ -188,6 +188,19 @@ func (r *SessionRouter) Create(ae *env.AppEnv, rc *response.RequestContext, para } entity := MapCreateSessionToModel(identity.Id, apiSession.Id, params.Session) + + // Legacy sessions are backed by a durable record, and the JWT must be signed with that + // record's id. Create must run before CreateJwt: its dedup can resolve entity.Id to a + // pre-existing session for the same (api-session, type, service) rather than the freshly + // generated id. Signing first would mint a token whose id has no backing session, so every + // subsequent create-circuit/create-terminator would fail to load the session. + if rc.HasLegacySecurityToken() { + if _, err = ae.Managers.Session.Create(entity, rc.NewChangeContext()); err != nil { + rc.RespondWithError(err) + return + } + } + jwtStr, err := ae.Managers.Session.CreateJwt(entity, rc.HasLegacySecurityToken()) if err != nil { @@ -217,14 +230,6 @@ func (r *SessionRouter) Create(ae *env.AppEnv, rc *response.RequestContext, para Meta: &rest_model.Meta{}, } - if rc.HasLegacySecurityToken() { - _, err = ae.Managers.Session.Create(entity, rc.NewChangeContext()) - if err != nil { - rc.RespondWithError(err) - return - } - } - rc.Respond(newSessionEnvelope, http.StatusCreated) r.createTimer.UpdateSince(start) diff --git a/controller/internal/routes/version_router.go b/controller/internal/routes/version_router.go index 25f5ec4f7..2a32745db 100644 --- a/controller/internal/routes/version_router.go +++ b/controller/internal/routes/version_router.go @@ -97,6 +97,7 @@ func (ir *VersionRouter) buildVersions(ae *env.AppEnv) *rest_model.Version { Version: buildInfo.Version(), APIVersions: map[string]map[string]rest_model.APIVersion{}, Capabilities: []string{}, + BuildFlags: build.GetBuildFlags(), } for apiBinding, apiVersionToPathMap := range webapis.AllApiBindingVersions { diff --git a/controller/model/command.go b/controller/model/command.go index 9ad0a08ab..9e31bdbf1 100644 --- a/controller/model/command.go +++ b/controller/model/command.go @@ -42,11 +42,10 @@ const ( ) func newCommandManager(env Env, registry ioc.Registry) *CommandManager { - command.GetDefaultDecoders().Clear() result := &CommandManager{ env: env, registry: registry, - Decoders: command.GetDefaultDecoders(), + Decoders: env.GetCommandDispatcher().GetDecoders(), backgroundDelayThreshold: env.GetConfig().Command.Background.DelayThreshold, backgroundWorkTimer: env.GetMetricsRegistry().Timer(backgroundQueueMetricsBase + ".work_timer"), } diff --git a/controller/model/connection_tracker_test.go b/controller/model/connection_tracker_test.go new file mode 100644 index 000000000..50e1ef852 --- /dev/null +++ b/controller/model/connection_tracker_test.go @@ -0,0 +1,214 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" + + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/openziti/ziti/v2/controller/event" +) + +// fakeCtrlChannel implements the part of ctrlchan.CtrlChannel that the connection tracker +// uses. The embedded interface is left nil so that any other method panics rather than +// quietly returning a zero value. +type fakeCtrlChannel struct { + ctrlchan.CtrlChannel + id string + closed atomic.Bool +} + +func (self *fakeCtrlChannel) PeerId() string { + return self.id +} + +func (self *fakeCtrlChannel) IsClosed() bool { + return self.closed.Load() +} + +func newTestConnectionTracker(t *testing.T) *ConnectionTracker { + closeNotify := make(chan struct{}) + t.Cleanup(func() { + close(closeNotify) + }) + + return &ConnectionTracker{ + connections: cmap.New[*identityConnections](), + eventDispatcher: event.DispatcherMock{}, + scanInterval: time.Millisecond, + unknownTimeout: time.Minute, + closeNotify: closeNotify, + } +} + +// TestConnectionTrackerLockOrdering ensures that the scan loop and connect/disconnect +// handling can run concurrently against the same identities without deadlocking. +// +// The tracker uses two locks, the cmap shard lock and the per-identity lock. Acquiring +// them in opposite orders in different code paths deadlocks permanently, and because the +// shard lock is then never released, every identity read in the controller blocks behind +// it. +func TestConnectionTrackerLockOrdering(t *testing.T) { + tracker := newTestConnectionTracker(t) + + // A small identity set keeps the scan loop and the connect/disconnect handling + // contending on the same entries and the same cmap shards. + identityIds := []string{"identity-1", "identity-2", "identity-3", "identity-4"} + + var stop atomic.Bool + var wg sync.WaitGroup + + for _, identityId := range identityIds { + wg.Add(1) + go func(identityId string) { + defer wg.Done() + ch := &fakeCtrlChannel{id: "router-1"} + for !stop.Load() { + // leaves the entry with no routers, making it a candidate for the scan + // loop to reap while this goroutine is still touching it + tracker.MarkConnected(identityId, ch) + tracker.MarkDisconnected(identityId, ch) + } + }(identityId) + } + + // An identity no other goroutine touches, so that the scan loop is the only thing + // that could take it offline. Nothing may drop an entry that has a live router + // connection, so if the scan loop removes it based on a view taken before the + // reconnect, this sees it. + var sawUnexpectedState atomic.Bool + wg.Add(1) + go func() { + defer wg.Done() + ch := &fakeCtrlChannel{id: "router-2"} + for !stop.Load() { + tracker.MarkConnected("identity-reconnecting", ch) + if tracker.GetIdentityOnlineState("identity-reconnecting") != IdentityStateOnline { + sawUnexpectedState.Store(true) + return + } + tracker.MarkDisconnected("identity-reconnecting", ch) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + for !stop.Load() { + tracker.ScanForDisconnectedRouters() + } + }() + + time.AfterFunc(2*time.Second, func() { + stop.Store(true) + }) + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(30 * time.Second): + buf := make([]byte, 4*1024*1024) + n := runtime.Stack(buf, true) + t.Fatalf("connection tracker deadlocked, goroutine dump follows:\n%s", buf[:n]) + } + + require.False(t, sawUnexpectedState.Load(), + "identity reported as not online while it had a live router connection") +} + +// TestConnectionTrackerReapsDisconnectedIdentities covers the entry removal path that the +// scan loop uses, since that is where the lock ordering has to be respected. +func TestConnectionTrackerReapsDisconnectedIdentities(t *testing.T) { + req := require.New(t) + tracker := newTestConnectionTracker(t) + + ch := &fakeCtrlChannel{id: "router-1"} + + tracker.MarkConnected("identity-1", ch) + req.Equal(IdentityStateOnline, tracker.GetIdentityOnlineState("identity-1")) + + tracker.ScanForDisconnectedRouters() + req.Equal(1, tracker.connections.Count(), "identity with a connected router should not be reaped") + + tracker.MarkDisconnected("identity-1", ch) + req.Equal(IdentityStateOffline, tracker.GetIdentityOnlineState("identity-1")) + + tracker.ScanForDisconnectedRouters() + req.Equal(0, tracker.connections.Count(), "identity with no connected routers should be reaped") +} + +// TestConnectionTrackerRemoveIfEmptyRechecksMapValue covers the window between the scan +// loop deciding an entry is empty and the entry actually being removed. Nothing is locked +// across that window, so an identity can reconnect inside it and the removal has to +// notice. +// +// The scan loop is driven a step at a time here because that window cannot be held open +// from outside; removeIfEmpty is exactly the step that runs after the decision. +func TestConnectionTrackerRemoveIfEmptyRechecksMapValue(t *testing.T) { + req := require.New(t) + tracker := newTestConnectionTracker(t) + + ch := &fakeCtrlChannel{id: "router-1"} + + tracker.MarkConnected("identity-1", ch) + tracker.MarkDisconnected("identity-1", ch) + + // the state the scan loop observes, and acts on, before removing the entry + entry, found := tracker.connections.Get("identity-1") + req.True(found) + entry.RLock() + empty := len(entry.routers) == 0 + entry.RUnlock() + req.True(empty, "scan loop would decide to remove this entry") + + // the identity reconnects before the removal runs + tracker.MarkConnected("identity-1", ch) + + tracker.removeIfEmpty("identity-1") + + req.Equal(1, tracker.connections.Count(), "reconnected identity should not be removed") + req.Equal(IdentityStateOnline, tracker.GetIdentityOnlineState("identity-1")) +} + +// TestConnectionTrackerRemoveIfEmptyRemovesEmptyEntry is the other half of +// TestConnectionTrackerRemoveIfEmptyRechecksMapValue, so that the recheck cannot be +// satisfied by simply never removing anything. +func TestConnectionTrackerRemoveIfEmptyRemovesEmptyEntry(t *testing.T) { + req := require.New(t) + tracker := newTestConnectionTracker(t) + + ch := &fakeCtrlChannel{id: "router-1"} + + tracker.MarkConnected("identity-1", ch) + tracker.MarkDisconnected("identity-1", ch) + req.Equal(1, tracker.connections.Count()) + + tracker.removeIfEmpty("identity-1") + req.Equal(0, tracker.connections.Count()) +} diff --git a/controller/model/controller_manager.go b/controller/model/controller_manager.go index 9993403a9..6261763a5 100644 --- a/controller/model/controller_manager.go +++ b/controller/model/controller_manager.go @@ -19,11 +19,11 @@ package model import ( "crypto/x509" "fmt" + "strings" "time" "github.com/michaelquigley/pfxlog" nfpem "github.com/openziti/foundation/v2/pem" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/edge_cmd_pb" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/command" @@ -31,6 +31,7 @@ import ( "github.com/openziti/ziti/v2/controller/event" "github.com/openziti/ziti/v2/controller/fields" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/boltz" "google.golang.org/protobuf/proto" ) @@ -291,7 +292,7 @@ func (self *ControllerManager) UpdateControllerState(peers []*event.ClusterPeer, Id: peer.Id, }, Name: peer.ServerCert[0].Subject.CommonName, - CertPem: nfpem.EncodeToString(peer.ServerCert[0]), + CertPem: certChainPem(peer.ServerCert), Fingerprint: nfpem.FingerprintFromCertificate(peer.ServerCert[0]), CtrlAddress: peer.Addr, IsOnline: true, @@ -403,7 +404,7 @@ func (self *ControllerManager) UpdateSelfOnNewLeader() { Id: peer.Id, }, Name: peer.ServerCert[0].Subject.CommonName, - CertPem: nfpem.EncodeToString(peer.ServerCert[0]), + CertPem: certChainPem(peer.ServerCert), Fingerprint: nfpem.FingerprintFromCertificate(peer.ServerCert[0]), CtrlAddress: peer.Addr, IsOnline: true, @@ -411,15 +412,15 @@ func (self *ControllerManager) UpdateSelfOnNewLeader() { ApiAddresses: apiAddressesFromPeer(peer), } disconnectFields := fields.UpdatedFieldsMap{ - db.FieldControllerIsOnline: struct{}{}, - db.FieldControllerCertPem: struct{}{}, - db.FieldControllerFingerprint: struct{}{}, - db.FieldControllerCtrlAddress: struct{}{}, - db.FieldControllerApiAddresses: struct{}{}, - db.FieldControllerApiAddressUrl: struct{}{}, - db.FieldControllerApiAddressVersion: struct{}{}, - db.FieldControllerIsPreferredLeader: struct{}{}, - db.FieldName: struct{}{}, + db.FieldControllerIsOnline: struct{}{}, + db.FieldControllerCertPem: struct{}{}, + db.FieldControllerFingerprint: struct{}{}, + db.FieldControllerCtrlAddress: struct{}{}, + db.FieldControllerApiAddresses: struct{}{}, + db.FieldControllerApiAddressUrl: struct{}{}, + db.FieldControllerApiAddressVersion: struct{}{}, + db.FieldControllerIsPreferredLeader: struct{}{}, + db.FieldName: struct{}{}, } changeCtx := change.New() @@ -432,6 +433,15 @@ func (self *ControllerManager) UpdateSelfOnNewLeader() { } // apiAddressFromPeer converts event.ClusterPeer API Addresses to model API Addresses +// certChainPem encodes certs (leaf first) as concatenated PEM. +func certChainPem(certs []*x509.Certificate) string { + sb := strings.Builder{} + for _, cert := range certs { + sb.WriteString(nfpem.EncodeToString(cert)) + } + return sb.String() +} + func apiAddressesFromPeer(peer *event.ClusterPeer) map[string][]ApiAddress { result := map[string][]ApiAddress{} diff --git a/controller/model/external_jwt_signer_manager.go b/controller/model/external_jwt_signer_manager.go index 386cdc5e5..ccc5bbef6 100644 --- a/controller/model/external_jwt_signer_manager.go +++ b/controller/model/external_jwt_signer_manager.go @@ -17,14 +17,18 @@ package model import ( - "github.com/openziti/ziti/v2/controller/storage/ast" - "github.com/openziti/ziti/v2/controller/storage/boltz" + "strings" + + "github.com/openziti/foundation/v2/errorz" "github.com/openziti/ziti/v2/common/pb/edge_cmd_pb" + "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/command" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/fields" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/ast" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/pkg/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -33,6 +37,7 @@ import ( func NewExternalJwtSignerManager(env Env) *ExternalJwtSignerManager { manager := &ExternalJwtSignerManager{ baseEntityManager: newBaseEntityManager[*ExternalJwtSigner, *db.ExternalJwtSigner](env, env.GetStores().ExternalJwtSigner), + jwksFetchPolicy: NewJwksFetchPolicy(JwksFetchConfig(env)), } manager.impl = manager @@ -43,6 +48,10 @@ func NewExternalJwtSignerManager(env Env) *ExternalJwtSignerManager { type ExternalJwtSignerManager struct { baseEntityManager[*ExternalJwtSigner, *db.ExternalJwtSigner] + + // jwksFetchPolicy is used to reject a jwksEndpoint at create/update time that could never + // be fetched. The same policy governs the fetch itself. + jwksFetchPolicy *JwksFetchPolicy } func (self *ExternalJwtSignerManager) NewModelEntity() *ExternalJwtSigner { @@ -50,6 +59,10 @@ func (self *ExternalJwtSignerManager) NewModelEntity() *ExternalJwtSigner { } func (self *ExternalJwtSignerManager) Create(entity *ExternalJwtSigner, ctx *change.Context) error { + if err := self.validateJwksEndpoint(entity); err != nil { + return err + } + return DispatchCreate[*ExternalJwtSigner](self, entity, ctx) } @@ -59,9 +72,29 @@ func (self *ExternalJwtSignerManager) ApplyCreate(cmd *command.CreateEntityComma } func (self *ExternalJwtSignerManager) Update(entity *ExternalJwtSigner, checker fields.UpdatedFields, ctx *change.Context) error { + if err := self.validateJwksEndpoint(entity); err != nil { + return err + } + return DispatchUpdate[*ExternalJwtSigner](self, entity, checker, ctx) } +// validateJwksEndpoint rejects a jwksEndpoint that the configured jwks fetch policy could +// never fetch, so the operator finds out on create/update rather than on a failed fetch. An +// endpoint that passes here can still be refused at fetch time, which is where the +// authoritative check lives. +func (self *ExternalJwtSignerManager) validateJwksEndpoint(entity *ExternalJwtSigner) error { + if entity.JwksEndpoint == nil || strings.TrimSpace(*entity.JwksEndpoint) == "" { + return nil + } + + if err := self.jwksFetchPolicy.ValidateEndpoint(*entity.JwksEndpoint); err != nil { + return apierror.NewBadRequestFieldError(*errorz.NewFieldError(err.Error(), db.FieldExternalJwtSignerJwksEndpoint, *entity.JwksEndpoint)) + } + + return nil +} + func (self *ExternalJwtSignerManager) ApplyUpdate(cmd *command.UpdateEntityCommand[*ExternalJwtSigner], ctx boltz.MutateContext) error { return self.updateEntity(cmd.Entity, cmd.UpdatedFields, ctx) } diff --git a/controller/model/identity_manager.go b/controller/model/identity_manager.go index c264938de..609de7236 100644 --- a/controller/model/identity_manager.go +++ b/controller/model/identity_manager.go @@ -1094,6 +1094,14 @@ const ( IdentityStateUnknown IdentityOnlineState = 2 ) +// identityConnections tracks which routers an identity is currently connected to, along +// with the last connectivity state reported for it. +// +// Two locks protect the connection tracker: the shard lock inside the ConcurrentMap of +// these, and the lock on each of these. They must always be acquired in that order, +// shard lock first. The cmap Upsert and RemoveCb callbacks run with the shard lock held, +// so taking this lock inside one of them is fine; taking the shard lock while already +// holding this one is not, and will deadlock. type identityConnections struct { sync.RWMutex routers map[string]ctrlchan.CtrlChannel @@ -1186,19 +1194,36 @@ func (self *ConnectionTracker) ScanForDisconnectedRouters() { } } - entry.Val.Lock() - if len(entry.Val.routers) == 0 { - self.connections.RemoveCb(entry.Key, func(key string, v *identityConnections, exists bool) bool { - if v != nil { - return len(v.routers) == 0 - } - return true - }) + // This check is only a filter, so that we don't take the shard write lock for + // every identity on every scan. The entry lock must be released before calling + // removeIfEmpty, which takes the shard lock. + entry.Val.RLock() + empty := len(entry.Val.routers) == 0 + entry.Val.RUnlock() + + if empty { + self.removeIfEmpty(entry.Key) } - entry.Val.Unlock() } } +// removeIfEmpty drops an identity's connection entry if it has no router connections. +// The identity may have reconnected since the caller decided the entry was empty, so the +// value currently in the map is what decides, not whatever the caller was looking at. +// +// This takes the shard lock, so it must not be called while holding an +// identityConnections lock. See identityConnections for the lock ordering rules. +func (self *ConnectionTracker) removeIfEmpty(identityId string) { + self.connections.RemoveCb(identityId, func(key string, v *identityConnections, exists bool) bool { + if v == nil { + return true + } + v.RLock() + defer v.RUnlock() + return len(v.routers) == 0 + }) +} + func (self *ConnectionTracker) MarkConnected(identityId string, ch ctrlchan.CtrlChannel) { pfxlog.Logger().WithField("identityId", identityId).WithField("routerId", ch.PeerId()).Trace("marking identity connected to router") var postUpsertCallback func() diff --git a/controller/model/jwks_resolver.go b/controller/model/jwks_resolver.go new file mode 100644 index 000000000..048b90210 --- /dev/null +++ b/controller/model/jwks_resolver.go @@ -0,0 +1,392 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "syscall" + + "github.com/openziti/jwks" + "github.com/openziti/ziti/v2/controller/config" + "github.com/openziti/ziti/v2/controller/db" +) + +// builtInBlockedCidrs are always refused for a JWKS fetch and cannot be re-enabled by +// configuration. They are the addresses that turn the controller's network position into a +// credential oracle, plus the addresses that have no meaning as an IdP endpoint. +var builtInBlockedCidrs = []string{ + "169.254.0.0/16", // IPv4 link-local, includes cloud instance metadata (169.254.169.254) and ECS task metadata (169.254.170.2) + "fe80::/10", // IPv6 link-local + "fd00:ec2::254/128", // AWS instance metadata over IPv6, inside unique-local space + "224.0.0.0/24", // IPv4 link-local multicast + "ff02::/16", // IPv6 link-local multicast + "0.0.0.0/32", // IPv4 unspecified + "::/128", // IPv6 unspecified +} + +// JwksFetchConfig returns the [edge.externalJwtSigners.jwksFetch] settings from the +// environment, falling back to the defaults when no edge configuration is present. +func JwksFetchConfig(env Env) config.JwksFetch { + if cfg := env.GetConfig(); cfg != nil && cfg.Edge != nil { + return cfg.Edge.ExternalJwtSigners.JwksFetch + } + + return config.DefaultJwksFetch() +} + +// JwksFetchPolicy decides whether the controller may fetch a given JWKS endpoint. The +// endpoint URL is operator-supplied, so without this the controller's network position would +// be reachable by whoever can write an external JWT signer. +// +// A hop is fetched only if it passes both gates, and both gates are applied to the initial +// request and to every redirect. Neither gate can authorize what the other refuses: host +// matching only ever narrows what may be fetched. +// +// CheckHostname is the hostname gate, applied to the URL's hostname: +// +// 1. deniedHostnames - blocked +// 2. allowedHostnames - when non-empty and the host does not match, blocked +// 3. anything else - passes +// +// CheckIP is the address gate, applied to the address being connected to, first-match-wins, +// deny before allow: +// +// 1. builtInBlockedCidrs - blocked, not overridable +// 2. deniedIPs - blocked, not overridable by allowedIPs +// 3. allowedIPs - allowed, a carve-out of tier 4 only +// 4. blockPrivateAddresses and the address is private or loopback - blocked +// 5. anything else - allowed +type JwksFetchPolicy struct { + blockPrivateAddresses bool + builtInBlocked []*net.IPNet + deniedIPs []*net.IPNet + allowedIPs []*net.IPNet + deniedHostnames []string + allowedHostnames []string +} + +// NewJwksFetchPolicy returns a JwksFetchPolicy built from the [edge.externalJwtSigners.jwksFetch] +// configuration section. +func NewJwksFetchPolicy(cfg config.JwksFetch) *JwksFetchPolicy { + result := &JwksFetchPolicy{ + blockPrivateAddresses: cfg.BlockPrivateAddresses, + deniedIPs: cfg.DeniedIPs, + allowedIPs: cfg.AllowedIPs, + deniedHostnames: cfg.DeniedHostnames, + allowedHostnames: cfg.AllowedHostnames, + } + + for _, cidr := range builtInBlockedCidrs { + _, ipNet, err := net.ParseCIDR(cidr) + + if err != nil { + // builtInBlockedCidrs is a compile-time constant list, a parse failure is a bug + panic(fmt.Errorf("invalid built-in blocked CIDR %s: %w", cidr, err)) + } + + result.builtInBlocked = append(result.builtInBlocked, ipNet) + } + + return result +} + +// CheckHostname returns nil if the controller may fetch from the given URL hostname, and an +// error otherwise. It is applied to the initial endpoint and to every redirect hop. +// +// Hostname matching narrows only. A hostname that passes here is still subject to CheckIP, and +// a caller may be able to reach the same target under a different name, so this is a filter on +// top of the address gate rather than a boundary of its own. +func (self *JwksFetchPolicy) CheckHostname(hostname string) error { + normalized := config.NormalizeHostname(hostname) + + if normalized == "" { + return fmt.Errorf("jwks endpoint url must include a hostname") + } + + if matchesHostname(self.deniedHostnames, normalized) { + return fmt.Errorf("jwks endpoint hostname %s is not permitted, it matches [edge.externalJwtSigners.jwksFetch.deniedHostnames]", normalized) + } + + if len(self.allowedHostnames) > 0 && !matchesHostname(self.allowedHostnames, normalized) { + return fmt.Errorf("jwks endpoint hostname %s is not permitted, [edge.externalJwtSigners.jwksFetch.allowedHostnames] is set and does not include it", normalized) + } + + return nil +} + +// matchesHostname reports whether a normalized hostname matches any of the given normalized +// patterns. A pattern is either an exact hostname, or a "*.suffix" wildcard that matches any +// subdomain of suffix at any depth but never suffix itself: "*.sub.host.com" matches +// "idp.sub.host.com" and "a.b.sub.host.com", but not "sub.host.com". +func matchesHostname(patterns []string, hostname string) bool { + for _, pattern := range patterns { + if suffix, isWildcard := strings.CutPrefix(pattern, "*."); isWildcard { + if strings.HasSuffix(hostname, "."+suffix) { + return true + } + + continue + } + + if hostname == pattern { + return true + } + } + + return false +} + +// CheckIP returns nil if the controller may connect to the given address for a JWKS fetch, +// and an error describing which tier refused it otherwise. An address that cannot be +// classified is refused. +func (self *JwksFetchPolicy) CheckIP(ip net.IP) error { + if ip == nil { + return fmt.Errorf("jwks endpoint address could not be parsed") + } + + // classify IPv4 and IPv4-mapped IPv6 addresses identically + if ip4 := ip.To4(); ip4 != nil { + ip = ip4 + } + + if containsIP(self.builtInBlocked, ip) { + return fmt.Errorf("jwks endpoint address %s is not permitted, it is a metadata, link-local or unspecified address", ip) + } + + if containsIP(self.deniedIPs, ip) { + return fmt.Errorf("jwks endpoint address %s is not permitted, it matches [edge.externalJwtSigners.jwksFetch.deniedIPs]", ip) + } + + if containsIP(self.allowedIPs, ip) { + return nil + } + + if self.blockPrivateAddresses && (ip.IsPrivate() || ip.IsLoopback()) { + return fmt.Errorf("jwks endpoint address %s is not permitted, private and loopback addresses are blocked by [edge.externalJwtSigners.jwksFetch.blockPrivateAddresses]", ip) + } + + return nil +} + +// ValidateEndpoint checks an operator-supplied jwksEndpoint URL at create/update time so an +// obviously unusable endpoint is reported immediately instead of failing later during a +// fetch. It rejects a scheme other than http/https, a missing host, and a host that is a +// literal blocked IP address. +// +// This is advisory: no name resolution happens here, so a hostname that resolves to a blocked +// address passes this check and is refused by the dialer at fetch time. The dial-time check +// remains the authoritative one. +func (self *JwksFetchPolicy) ValidateEndpoint(endpoint string) error { + target, err := url.Parse(strings.TrimSpace(endpoint)) + + if err != nil { + return fmt.Errorf("could not parse jwks endpoint url: %w", err) + } + + if err = validateJwksEndpointScheme(target); err != nil { + return err + } + + host := target.Hostname() + + if err = self.CheckHostname(host); err != nil { + return err + } + + if ip := net.ParseIP(host); ip != nil { + return self.CheckIP(ip) + } + + return nil +} + +// checkJwksEndpointAllowed returns an error when a signer's jwksEndpoint is one the given +// policy refuses. A signer with no jwksEndpoint, or a blank one, is not reported. +func checkJwksEndpointAllowed(policy *JwksFetchPolicy, signer *db.ExternalJwtSigner) error { + if signer.JwksEndpoint == nil || strings.TrimSpace(*signer.JwksEndpoint) == "" { + return nil + } + + return policy.ValidateEndpoint(*signer.JwksEndpoint) +} + +// CheckDialAddress applies CheckIP to a host:port address as it is about to be dialed. The +// address has already been resolved at that point, which is what makes the check +// rebinding-safe: the address that is connected to is the address that is checked. +func (self *JwksFetchPolicy) CheckDialAddress(address string) error { + host, _, err := net.SplitHostPort(address) + + if err != nil { + return fmt.Errorf("could not parse jwks endpoint dial address %s: %w", address, err) + } + + return self.CheckIP(net.ParseIP(host)) +} + +// containsIP reports whether any of the given networks contains the given address. +func containsIP(ipNets []*net.IPNet, ip net.IP) bool { + for _, ipNet := range ipNets { + if ipNet.Contains(ip) { + return true + } + } + + return false +} + +var _ jwks.Resolver = (*HardenedJwksResolver)(nil) + +// HardenedJwksResolver fetches JWKS responses over HTTP(S) with the constraints an +// operator-supplied URL requires: a bounded total time, a bounded number of redirects, an +// http/https-only scheme, and a JwksFetchPolicy check of every address that is connected to. +// +// The address check is installed as the dialer's control function, so it is the single choke +// point for the first request and for every redirect hop: it runs after DNS resolution and +// before the connection is made, which is what a check of the URL's hostname cannot do. +type HardenedJwksResolver struct { + client *http.Client + policy *JwksFetchPolicy +} + +// NewHardenedJwksResolver returns a HardenedJwksResolver configured from the +// [edge.externalJwtSigners.jwksFetch] configuration section. +func NewHardenedJwksResolver(cfg config.JwksFetch) *HardenedJwksResolver { + if cfg.Timeout <= 0 { + cfg.Timeout = config.DefaultJwksFetchTimeout + } + + if cfg.MaxRedirects < 0 { + cfg.MaxRedirects = config.DefaultJwksFetchMaxRedirects + } + + policy := NewJwksFetchPolicy(cfg) + + dialer := &net.Dialer{ + Timeout: cfg.Timeout, + Control: func(_ string, address string, _ syscall.RawConn) error { + return policy.CheckDialAddress(address) + }, + } + + transport := &http.Transport{ + DialContext: dialer.DialContext, + TLSHandshakeTimeout: cfg.Timeout, + // JWKS fetches are infrequent, so take a fresh, policy-checked connection every time + // rather than reusing one + DisableKeepAlives: true, + } + + maxRedirects := cfg.MaxRedirects + + client := &http.Client{ + Timeout: cfg.Timeout, + Transport: transport, + CheckRedirect: func(request *http.Request, via []*http.Request) error { + if len(via) > maxRedirects { + return fmt.Errorf("jwks endpoint exceeded the maximum of %d redirect(s)", maxRedirects) + } + + if err := validateJwksEndpointScheme(request.URL); err != nil { + return err + } + + // every hop is checked on its own, an allowed first hop does not carry over + return policy.CheckHostname(request.URL.Hostname()) + }, + } + + return &HardenedJwksResolver{ + client: client, + policy: policy, + } +} + +// Get implements jwks.Resolver. It returns the parsed JWKS response and the raw response body. +func (self *HardenedJwksResolver) Get(endpoint string) (*jwks.Response, []byte, error) { + target, err := url.Parse(strings.TrimSpace(endpoint)) + + if err != nil { + return nil, nil, fmt.Errorf("could not parse jwks endpoint url: %w", err) + } + + if err = validateJwksEndpointScheme(target); err != nil { + return nil, nil, err + } + + if err = self.policy.CheckHostname(target.Hostname()); err != nil { + return nil, nil, err + } + + request, err := http.NewRequest(http.MethodGet, target.String(), nil) + + if err != nil { + return nil, nil, fmt.Errorf("could not create jwks endpoint request: %w", err) + } + + request.Header.Set("accept", "application/json") + + response, err := self.client.Do(request) + + if err != nil { + return nil, nil, err + } + + defer func() { + _ = response.Body.Close() + }() + + if response.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("could not fetch JWKS, status code was not 200 OK, got %d", response.StatusCode) + } + + contentType := strings.ToLower(strings.TrimSpace(strings.Split(response.Header.Get("content-type"), ";")[0])) + + if contentType != "application/json" && contentType != "application/jwk-set+json" && contentType != "application/jwk+json" { + return nil, nil, fmt.Errorf("invalid content type %s, expected application/json", contentType) + } + + body, err := io.ReadAll(response.Body) + + if err != nil { + return nil, nil, fmt.Errorf("could not read jwks response: %w", err) + } + + jwksResponse := &jwks.Response{} + + if err = json.Unmarshal(body, jwksResponse); err != nil { + return nil, nil, fmt.Errorf("could not parse jwks response: %w", err) + } + + return jwksResponse, body, nil +} + +// validateJwksEndpointScheme allows only http and https. Anything else either cannot be +// fetched or would let the URL scheme choose the transport. +func validateJwksEndpointScheme(target *url.URL) error { + switch strings.ToLower(target.Scheme) { + case "http", "https": + return nil + } + + return fmt.Errorf("invalid jwks endpoint scheme %s, only http and https are supported", target.Scheme) +} diff --git a/controller/model/jwks_resolver_test.go b/controller/model/jwks_resolver_test.go new file mode 100644 index 000000000..57c73565b --- /dev/null +++ b/controller/model/jwks_resolver_test.go @@ -0,0 +1,848 @@ +package model + +import ( + "net" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/openziti/ziti/v2/controller/config" + "github.com/openziti/ziti/v2/controller/db" + "github.com/stretchr/testify/require" +) + +func Test_JwksFetchPolicy_CheckIP(t *testing.T) { + t.Run("with default settings", func(t *testing.T) { + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + t.Run("built-in blocked addresses are blocked", func(t *testing.T) { + blocked := []string{ + "169.254.169.254", // cloud instance metadata + "169.254.170.2", // ECS task metadata + "fd00:ec2::254", // AWS IMDS over IPv6 + "169.254.10.10", // link-local + "fe80::1", // link-local + "224.0.0.1", // link-local multicast + "ff02::1", // link-local multicast + "0.0.0.0", // unspecified + "::", // unspecified + } + + for _, address := range blocked { + t.Run(address, func(t *testing.T) { + req := require.New(t) + + err := policy.CheckIP(net.ParseIP(address)) + + req.Error(err, "%s must be blocked by the built-in tier", address) + }) + } + }) + + t.Run("private and loopback addresses are allowed, the default posture is compatible", func(t *testing.T) { + allowed := []string{"127.0.0.1", "::1", "10.1.2.3", "172.16.0.1", "192.168.1.1", "fc00::1"} + + for _, address := range allowed { + t.Run(address, func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckIP(net.ParseIP(address))) + }) + } + }) + + t.Run("a public address is allowed", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckIP(net.ParseIP("93.184.216.34"))) + }) + }) + + t.Run("with blockPrivateAddresses enabled", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("private and loopback addresses are blocked", func(t *testing.T) { + blocked := []string{"127.0.0.1", "::1", "10.1.2.3", "172.16.0.1", "192.168.1.1", "fc00::1"} + + for _, address := range blocked { + t.Run(address, func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckIP(net.ParseIP(address))) + }) + } + }) + + t.Run("a public address is still allowed", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckIP(net.ParseIP("93.184.216.34"))) + }) + }) + + t.Run("deniedIPs blocks an otherwise allowed public address", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedIPs = mustCidrs(t, "203.0.113.0/24") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("203.0.113.5"))) + req.NoError(policy.CheckIP(net.ParseIP("203.0.114.5"))) + }) + + t.Run("allowedIPs carves an exception out of blockPrivateAddresses", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + jwksFetch.AllowedIPs = mustCidrs(t, "10.1.0.0/16") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.NoError(policy.CheckIP(net.ParseIP("10.1.2.3")), "the carve-out address must be reachable") + req.Error(policy.CheckIP(net.ParseIP("10.2.2.3")), "a private address outside the carve-out must stay blocked") + }) + + t.Run("allowedIPs cannot override the built-in blocked tier", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedIPs = mustCidrs(t, "169.254.0.0/16", "fd00:ec2::/64") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("169.254.169.254")), "the metadata service must never be reachable") + req.Error(policy.CheckIP(net.ParseIP("fd00:ec2::254")), "the IPv6 metadata service must never be reachable") + }) + + t.Run("allowedIPs cannot override deniedIPs", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedIPs = mustCidrs(t, "10.0.0.0/8") + jwksFetch.AllowedIPs = mustCidrs(t, "10.1.2.3/32") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("10.1.2.3")), "deny wins over allow") + }) + + t.Run("an IPv4-mapped IPv6 address is classified as its IPv4 address", func(t *testing.T) { + t.Run("a mapped metadata address is blocked", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.Error(policy.CheckIP(net.ParseIP("::ffff:169.254.169.254"))) + }) + + t.Run("a mapped private address is blocked when blockPrivateAddresses is set", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("::ffff:10.1.2.3"))) + }) + + t.Run("a mapped address matches an IPv4 allowedIPs carve-out", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + jwksFetch.AllowedIPs = mustCidrs(t, "10.1.0.0/16") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.NoError(policy.CheckIP(net.ParseIP("::ffff:10.1.2.3"))) + }) + + t.Run("a mapped address matches an IPv4 deniedIPs entry", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedIPs = mustCidrs(t, "203.0.113.0/24") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("::ffff:203.0.113.5"))) + }) + }) + + t.Run("an unparsable address is blocked", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.Error(policy.CheckIP(nil), "an address that could not be parsed must never be treated as allowed") + }) +} + +func Test_JwksFetchPolicy_CheckHostname(t *testing.T) { + t.Run("with no host lists configured every host passes", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.NoError(policy.CheckHostname("idp.example.com")) + req.NoError(policy.CheckHostname("10.0.0.5")) + }) + + t.Run("an empty host is blocked", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.Error(policy.CheckHostname("")) + }) + + t.Run("deniedHostnames blocks a matching host", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"blocked.example.com", "*.internal.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("an exact match is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("blocked.example.com")) + }) + + t.Run("a wildcard suffix match is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("idp.internal.example.com")) + req.Error(policy.CheckHostname("deep.idp.internal.example.com")) + }) + + t.Run("the wildcard suffix itself is not matched", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckHostname("internal.example.com"), "*.internal.example.com covers subdomains only") + }) + + t.Run("a wildcard matches whole labels only", func(t *testing.T) { + req := require.New(t) + + // xinternal.example.com ends with internal.example.com as a string, but the + // wildcard replaces a whole label, so it is a different host + req.NoError(policy.CheckHostname("xinternal.example.com")) + req.NoError(policy.CheckHostname("notinternal.example.com")) + }) + + t.Run("an unlisted host passes", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckHostname("idp.example.com")) + }) + + t.Run("matching is case insensitive and ignores a trailing dot", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("BLOCKED.example.com")) + req.Error(policy.CheckHostname("blocked.example.com.")) + }) + }) + + t.Run("allowedHostnames is exclusive when set", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com", "*.idp.example.org"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("a listed host passes", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckHostname("idp.example.com")) + req.NoError(policy.CheckHostname("eu.idp.example.org")) + }) + + t.Run("an unlisted host is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("evil.example.com")) + }) + + t.Run("a literal IP host is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("93.184.216.34"), "an allowedHostnames list can only be satisfied by a name") + }) + }) + + t.Run("deniedHostnames wins over allowedHostnames", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"old.idp.example.com"} + jwksFetch.AllowedHostnames = []string{"*.idp.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckHostname("old.idp.example.com")) + req.NoError(policy.CheckHostname("new.idp.example.com")) + }) + + t.Run("an allowed host does not authorize a blocked address", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + req.NoError(policy.CheckHostname("idp.example.com")) + req.Error(policy.CheckIP(net.ParseIP("169.254.169.254")), "the hostname gate must never widen the address gate") + }) +} + +func Test_JwksFetchPolicy_ValidateEndpoint(t *testing.T) { + t.Run("with default settings", func(t *testing.T) { + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + t.Run("an http or https endpoint is accepted", func(t *testing.T) { + endpoints := []string{ + "https://idp.example.com/.well-known/jwks.json", + "http://idp.example.com/.well-known/jwks.json", + "HTTPS://idp.example.com/jwks", + } + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.ValidateEndpoint(endpoint)) + }) + } + }) + + t.Run("a non-http scheme is rejected", func(t *testing.T) { + endpoints := []string{"file:///etc/passwd", "ftp://idp.example.com/jwks", "idp.example.com/jwks", ""} + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint(endpoint)) + }) + } + }) + + t.Run("an endpoint without a host is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("http:///jwks")) + }) + + t.Run("a literal metadata address is rejected", func(t *testing.T) { + endpoints := []string{ + "http://169.254.169.254/latest/meta-data/", + "http://169.254.170.2/v2/credentials", + "http://[fd00:ec2::254]/latest/meta-data/", + "http://169.254.169.254:8080/jwks", + } + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint(endpoint)) + }) + } + }) + + t.Run("a literal private address is accepted, the default posture allows it", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.ValidateEndpoint("https://10.1.2.3/jwks")) + }) + }) + + t.Run("with blockPrivateAddresses enabled", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("a literal private address is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("https://10.1.2.3/jwks")) + }) + + t.Run("a hostname is accepted, the dial-time check remains authoritative", func(t *testing.T) { + req := require.New(t) + + // no DNS resolution happens at create/update time, so a hostname that resolves to + // a blocked address is caught by the dialer instead + req.NoError(policy.ValidateEndpoint("https://localhost/jwks")) + }) + + t.Run("a literal private address in allowedIPs is accepted", func(t *testing.T) { + req := require.New(t) + + carveOut := config.DefaultJwksFetch() + carveOut.BlockPrivateAddresses = true + carveOut.AllowedIPs = mustCidrs(t, "10.1.0.0/16") + + req.NoError(NewJwksFetchPolicy(carveOut).ValidateEndpoint("https://10.1.2.3/jwks")) + }) + }) + + t.Run("with host lists configured", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"old.idp.example.com"} + jwksFetch.AllowedHostnames = []string{"idp.example.com", "*.idp.example.org"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("an allowed host is accepted", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.ValidateEndpoint("https://idp.example.com/.well-known/jwks.json")) + req.NoError(policy.ValidateEndpoint("https://eu.idp.example.org/jwks")) + }) + + t.Run("a host outside allowedHostnames is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("https://other.example.com/jwks")) + }) + + t.Run("a denied host is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("https://old.idp.example.com/jwks")) + }) + }) +} + +func Test_checkJwksEndpointAllowed(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("a signer without a jwks endpoint is not reported", func(t *testing.T) { + req := require.New(t) + + req.NoError(checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{})) + }) + + t.Run("a signer with a blank jwks endpoint is not reported", func(t *testing.T) { + req := require.New(t) + + endpoint := " " + + req.NoError(checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{JwksEndpoint: &endpoint})) + }) + + t.Run("a signer with an allowed jwks endpoint is not reported", func(t *testing.T) { + req := require.New(t) + + endpoint := "https://idp.example.com/jwks" + + req.NoError(checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{JwksEndpoint: &endpoint})) + }) + + t.Run("a signer whose jwks endpoint the configuration now refuses is reported", func(t *testing.T) { + req := require.New(t) + + // the signer was created before the host list was set, so it is orphaned by the + // current configuration and the operator needs to know at startup + endpoint := "https://old-idp.example.com/jwks" + + err := checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{JwksEndpoint: &endpoint}) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + }) + + t.Run("a signer whose jwks endpoint is a blocked address is reported", func(t *testing.T) { + req := require.New(t) + + endpoint := "http://169.254.169.254/latest/meta-data/" + + req.Error(checkJwksEndpointAllowed(NewJwksFetchPolicy(config.DefaultJwksFetch()), + &db.ExternalJwtSigner{JwksEndpoint: &endpoint})) + }) +} + +func Test_HardenedJwksResolver_Get(t *testing.T) { + const jwksBody = `{"keys":[{"kty":"RSA","kid":"test-kid","n":"AQAB","e":"AQAB"}]}` + + t.Run("a valid JWKS endpoint resolves", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + response, body, err := resolver.Get(server.URL + "/jwks") + + req.NoError(err) + req.Len(response.Keys, 1) + req.Equal("test-kid", response.Keys[0].KeyId) + req.Equal(jwksBody, string(body)) + }) + + t.Run("a blocked address is refused without contacting the endpoint", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err := resolver.Get(server.URL + "/jwks") + + req.Error(err) + req.Contains(err.Error(), "is not permitted") + req.Zero(server.requestCount(), "the request must be stopped before it reaches the endpoint") + }) + + t.Run("a hostname that resolves to a blocked address is refused at dial time", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + // the URL host is a name, so only the resolved address can be checked - this is the + // DNS rebinding case, and it is why enforcement lives in the dialer + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err = resolver.Get(endpoint) + + req.Error(err) + req.Contains(err.Error(), "is not permitted") + req.Zero(server.requestCount(), "the request must be stopped before it reaches the endpoint") + }) + + t.Run("a redirect to a blocked address is refused", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/redirect-to-metadata") + + req.Error(err, "the metadata address must be blocked on a redirect hop, not just on the first hop") + req.Contains(err.Error(), "is not permitted", "the redirect hop must be refused by the address policy") + }) + + t.Run("a host that is not in allowedHostnames is refused without contacting the endpoint", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err = resolver.Get(endpoint) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + req.Zero(server.requestCount(), "the request must be stopped before it reaches the endpoint") + }) + + t.Run("a host in deniedHostnames is refused without contacting the endpoint", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"localhost"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err = resolver.Get(endpoint) + + req.Error(err) + req.Contains(err.Error(), "deniedHostnames") + req.Zero(server.requestCount()) + }) + + t.Run("a host in allowedHostnames is fetched", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"localhost"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + response, _, err := resolver.Get(endpoint) + + req.NoError(err) + req.Len(response.Keys, 1) + }) + + t.Run("a redirect to a host that is not in allowedHostnames is refused", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"localhost"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + // the first hop is an allowed host, the redirect target is not - each hop is checked + // on its own + _, _, err = resolver.Get(strings.Replace(endpoint, "/jwks", "/redirect-to-unlisted-host", 1)) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + }) + + t.Run("a redirect within the cap is followed", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + response, _, err := resolver.Get(server.URL + "/redirect-to-jwks") + + req.NoError(err) + req.Len(response.Keys, 1) + }) + + t.Run("exceeding maxRedirects is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.MaxRedirects = 2 + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err := resolver.Get(server.URL + "/redirect-loop") + + req.Error(err) + req.Contains(err.Error(), "redirect") + }) + + t.Run("maxRedirects of zero refuses to follow any redirect", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.MaxRedirects = 0 + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err := resolver.Get(server.URL + "/redirect-to-jwks") + + req.Error(err) + req.Contains(err.Error(), "redirect") + }) + + t.Run("a slow endpoint is cut off at the timeout", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.Timeout = 100 * time.Millisecond + + resolver := NewHardenedJwksResolver(jwksFetch) + + start := time.Now() + _, _, err := resolver.Get(server.URL + "/hang") + elapsed := time.Since(start) + + req.Error(err) + req.Less(elapsed, 10*time.Second, "the fetch must be bounded by the configured timeout") + }) + + t.Run("a non-http scheme is refused", func(t *testing.T) { + endpoints := []string{ + "file:///etc/passwd", + "ftp://example.com/jwks", + "gopher://example.com:70/jwks", + "/no/scheme/at/all", + } + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(endpoint) + + req.Error(err) + }) + } + }) + + t.Run("an unparsable url is refused", func(t *testing.T) { + req := require.New(t) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get("http://[::1") + + req.Error(err) + }) + + t.Run("a non-200 status is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/not-found") + + req.Error(err) + }) + + t.Run("a non-json content type is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/html") + + req.Error(err) + }) + + t.Run("a body that is not a JWKS response is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/not-json") + + req.Error(err) + }) +} + +// testJwksServer is a local endpoint that serves the routes the resolver tests exercise and +// counts the requests that reach it. +type testJwksServer struct { + *httptest.Server + requests atomic.Int32 +} + +// requestCount returns how many requests reached the server. +func (self *testJwksServer) requestCount() int { + return int(self.requests.Load()) +} + +// urlWithHost returns the server's URL with its host replaced, keeping the port. +func (self *testJwksServer) urlWithHost(host string) (string, error) { + parsed, err := url.Parse(self.URL) + + if err != nil { + return "", err + } + + parsed.Host = net.JoinHostPort(host, parsed.Port()) + + return parsed.String() + "/jwks", nil +} + +// newTestJwksServer starts a JWKS endpoint for the duration of the test. +func newTestJwksServer(t *testing.T, jwksBody string) *testJwksServer { + result := &testJwksServer{} + + // released on test cleanup so the /hang route does not outlive the test + done := make(chan struct{}) + + result.Server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + result.requests.Add(1) + + switch request.URL.Path { + case "/jwks": + writer.Header().Set("content-type", "application/json") + _, _ = writer.Write([]byte(jwksBody)) + case "/redirect-to-jwks": + http.Redirect(writer, request, "/jwks", http.StatusFound) + case "/redirect-loop": + http.Redirect(writer, request, "/redirect-loop", http.StatusFound) + case "/redirect-to-metadata": + http.Redirect(writer, request, "http://169.254.169.254/latest/meta-data/", http.StatusFound) + case "/redirect-to-unlisted-host": + http.Redirect(writer, request, "http://unlisted.example.com/jwks", http.StatusFound) + case "/hang": + <-done + case "/html": + writer.Header().Set("content-type", "text/html") + _, _ = writer.Write([]byte("")) + case "/not-json": + writer.Header().Set("content-type", "application/json") + _, _ = writer.Write([]byte("this is not json")) + default: + writer.WriteHeader(http.StatusNotFound) + } + })) + + t.Cleanup(func() { + close(done) + result.Close() + }) + + return result +} + +// mustCidrs parses CIDRs for test setup, failing the test if any cannot be parsed. +func mustCidrs(t *testing.T, values ...string) []*net.IPNet { + t.Helper() + + var result []*net.IPNet + + for _, value := range values { + _, ipNet, err := net.ParseCIDR(value) + + if err != nil { + t.Fatalf("could not parse test CIDR %s: %v", value, err) + } + + result = append(result, ipNet) + } + + return result +} diff --git a/controller/model/router_manager.go b/controller/model/router_manager.go index 9d254eba2..cbcf7d8a5 100644 --- a/controller/model/router_manager.go +++ b/controller/model/router_manager.go @@ -27,6 +27,9 @@ import ( "time" "github.com/openziti/channel/v4/protobufs" + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/concurrency" + "github.com/openziti/ziti/v2/common/ctrlchan" "github.com/openziti/ziti/v2/common/inspect" "github.com/openziti/ziti/v2/common/pb/cmd_pb" "github.com/openziti/ziti/v2/common/pb/ctrl_pb" @@ -38,9 +41,9 @@ import ( "google.golang.org/protobuf/proto" "github.com/michaelquigley/pfxlog" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/boltz" cmap "github.com/orcaman/concurrent-map/v2" "github.com/pkg/errors" "go.etcd.io/bbolt" @@ -79,8 +82,9 @@ func NewRouter(id, name, fingerprint string, cost uint16, noTraversal bool) *Rou type RouterManager struct { baseEntityManager[*Router, *db.Router] - cache cmap.ConcurrentMap[string, *Router] - connected cmap.ConcurrentMap[string, *Router] + cache cmap.ConcurrentMap[string, *Router] + connected cmap.ConcurrentMap[string, *Router] + connectLocks *concurrency.StripedIdLocker } func newRouterManager(env Env) *RouterManager { @@ -89,6 +93,7 @@ func newRouterManager(env Env) *RouterManager { baseEntityManager: newBaseEntityManager[*Router, *db.Router](env, routerStore), cache: cmap.New[*Router](), connected: cmap.New[*Router](), + connectLocks: concurrency.NewStripedIdLocker(256), } result.impl = result @@ -115,28 +120,61 @@ func (self *RouterManager) NewModelEntity() *Router { return &Router{} } -func (self *RouterManager) MarkConnected(r *Router) { - if router, _ := self.connected.Get(r.Id); router != nil { - if ch := router.Control; ch != nil { - if err := ch.Close(); err != nil { - pfxlog.Logger().WithError(err).Error("error closing control channel") - } +// LockConnectFor acquires the per-router connect lock for the given router id and returns the unlock +// function. It serializes a router's connect and disconnect processing so they cannot interleave. The +// returned unlock is idempotent, so a caller may both defer it (as a leak-safety net across all exit +// paths) and call it early (e.g. to release before closing a channel outside the lock) without +// double-unlocking. The flag is unshared and only ever touched by the goroutine holding the lock, so it +// needs no synchronization. +func (self *RouterManager) LockConnectFor(id string) func() { + rawUnlock := self.connectLocks.LockFor(id) + unlocked := false + return func() { + if !unlocked { + unlocked = true + rawUnlock() } } +} +// MarkConnected publishes r as the current connection for its router id. Callers must serialize with +// LockConnectFor and must have already ensured the slot is free (ConnectRouter rejects a busy slot rather +// than taking over here), so this only records the connection; it does not close any prior channel. +func (self *RouterManager) MarkConnected(r *Router) { r.Connected.Store(true) self.connected.Set(r.Id, r) } +// MarkDisconnected gives up r's registration, and does nothing at all if r is not the registration holder. +// +// A connection only owns the state on its own instance, so a connection that has already been replaced must +// not clear it. What makes that worth enforcing rather than assuming is where the connected flag is read: +// the handler for a router's link reports drops them when it is false, so clearing it for the wrong +// connection silences a router that is up and reporting, with nothing to correct it. +// +// This is all-or-nothing rather than a fix for instances shared between connections. Pointer identity +// cannot tell two connections apart when they share an instance, so in that case the shared instance is the +// registration holder and its state is cleared here regardless. Only giving each connection its own +// instance prevents that. func (self *RouterManager) MarkDisconnected(r *Router) { - r.Connected.Store(false) - self.connected.RemoveCb(r.Id, func(key string, v *Router, exists bool) bool { + removed := self.connected.RemoveCb(r.Id, func(key string, v *Router, exists bool) bool { if exists && v != r { pfxlog.Logger().WithField("routerId", r.Id).Info("router not current connect, not clearing from connected map") return false } - return exists + if !exists { + return false + } + // Under the shard lock so the flag and the map entry change together: the link report path looks the + // router up and then reads the flag, and would otherwise see a removed entry still marked connected. + r.Connected.Store(false) + return true }) + + if !removed { + return + } + r.routerLinks.Clear() } @@ -196,6 +234,46 @@ func (self *RouterManager) Exists(id string) (bool, error) { return exists, err } +// NewCtrlChanRouter builds the Router instance representing one control-channel connection, with the +// channel already recorded on it. +// +// The instance is deliberately kept out of the router cache. The connect path writes connection-scoped +// state onto it (Control, ConnectTime, VersionInfo, Connected, links), and the controller decides which of +// two racing connections for a router is current by comparing instances. Handing a cached instance to a +// second connection makes the two indistinguishable: neither the connect path's occupied-slot check nor the +// disconnect path's currency check can tell them apart, so a connection that dies takes its replacement's +// registration down with it, and each connection's writes land on the other's state. +// +// Read, by contrast, is cache-backed and returns router entity data. Connection state belongs to +// GetConnected, not to Read. +func (self *RouterManager) NewCtrlChanRouter(ch channel.Channel) (*Router, error) { + r, err := self.readUncached(ch.Id()) + if err != nil { + return nil, err + } + if r == nil { + return nil, fmt.Errorf("no router with id [%v] found", ch.Id()) + } + + multiCh, ok := ch.(channel.MultiChannel) + if !ok { + return nil, fmt.Errorf("control channel for router [%v] is not a multi-channel, got %T", ch.Id(), ch) + } + + ctrlCh, ok := multiCh.GetUnderlayHandler().(ctrlchan.CtrlChannel) + if !ok { + return nil, fmt.Errorf("control channel for router [%v] has unexpected underlay handler type %T", ch.Id(), multiCh.GetUnderlayHandler()) + } + + r.Control = ctrlCh + r.ConnectTime = time.Now() + + return r, nil +} + +// readUncached reads a router straight from the database, bypassing the cache in both directions: it +// neither reads a cached instance nor publishes the one it creates. Callers needing an instance they can +// own get it here. func (self *RouterManager) readUncached(id string) (*Router, error) { entity := &Router{} err := self.GetDb().View(func(tx *bbolt.Tx) error { diff --git a/controller/model/router_manager_test.go b/controller/model/router_manager_test.go new file mode 100644 index 000000000..cb5859408 --- /dev/null +++ b/controller/model/router_manager_test.go @@ -0,0 +1,83 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "testing" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" +) + +func newMarkDisconnectedTestManager() *RouterManager { + return &RouterManager{connected: cmap.New[*Router]()} +} + +// TestMarkDisconnected_LeavesStateOfNonHolderAlone: a connection owns only the state on its own instance, so +// one that has already been replaced must not clear it. The connected flag decides whether the controller +// accepts that router's link reports, so clearing it for the wrong connection silences a router that is up +// and reporting with nothing to correct it. +func TestMarkDisconnected_LeavesStateOfNonHolderAlone(t *testing.T) { + mgr := newMarkDisconnectedTestManager() + + superseded := NewRouter("r1", "r1", "", 0, false) + current := NewRouter("r1", "r1", "", 0, false) + + superseded.Connected.Store(true) + superseded.routerLinks.Add(&Link{Id: "l1", DstId: "dst"}, "dst") + + mgr.MarkConnected(current) + + mgr.MarkDisconnected(superseded) + + require.Same(t, current, mgr.GetConnected("r1"), "the registration holder must be left in place") + require.True(t, current.Connected.Load(), "the holder's connected flag must not be cleared") + require.True(t, superseded.Connected.Load(), + "a non-holder's state is not this call's to clear") + require.Len(t, superseded.GetLinks(), 1, "a non-holder's links are not this call's to clear") +} + +// TestMarkDisconnected_ClearsStateOfHolder is the ordinary case: the registration holder disconnecting gives +// up the registration and its per-connection state together. +func TestMarkDisconnected_ClearsStateOfHolder(t *testing.T) { + mgr := newMarkDisconnectedTestManager() + + current := NewRouter("r1", "r1", "", 0, false) + current.routerLinks.Add(&Link{Id: "l1", DstId: "dst"}, "dst") + mgr.MarkConnected(current) + require.True(t, current.Connected.Load()) + + mgr.MarkDisconnected(current) + + require.Nil(t, mgr.GetConnected("r1"), "the registration must be given up") + require.False(t, current.Connected.Load(), "the holder's connected flag must be cleared") + require.Empty(t, current.GetLinks(), "the holder's links must be cleared") +} + +// TestMarkDisconnected_UnregisteredIsANoop: a connection that never registered, or whose registration is +// already gone, has nothing to give up. +func TestMarkDisconnected_UnregisteredIsANoop(t *testing.T) { + mgr := newMarkDisconnectedTestManager() + + r := NewRouter("r1", "r1", "", 0, false) + r.Connected.Store(true) + + mgr.MarkDisconnected(r) + + require.Nil(t, mgr.GetConnected("r1")) + require.True(t, r.Connected.Load(), "with no registration to give up there is nothing to clear") +} diff --git a/controller/model/router_model.go b/controller/model/router_model.go index 55c586ef7..c88e5acf7 100644 --- a/controller/model/router_model.go +++ b/controller/model/router_model.go @@ -45,6 +45,9 @@ type Router struct { Control ctrlchan.CtrlChannel Connected atomic.Bool ConnectTime time.Time + // VersionInfo is reported in the router's hello and is not persisted, so it is only populated on the + // instance built for a control-channel connection. It is nil on an instance loaded from the database. + // Read it from GetConnected rather than from whatever instance is to hand. VersionInfo *versions.VersionInfo routerLinks RouterLinks Cost uint16 diff --git a/controller/model/token_provider_cache.go b/controller/model/token_provider_cache.go index e8cb0cb52..272878bc1 100644 --- a/controller/model/token_provider_cache.go +++ b/controller/model/token_provider_cache.go @@ -32,10 +32,10 @@ import ( nfPem "github.com/openziti/foundation/v2/pem" "github.com/openziti/foundation/v2/stringz" "github.com/openziti/jwks" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/storage/boltz" cmap "github.com/orcaman/concurrent-map/v2" "go.etcd.io/bbolt" ) @@ -54,6 +54,10 @@ type TokenIssuerCache struct { // due to xweb API address binds controllerIssuers cmap.ConcurrentMap[string, common.TokenIssuer] + // jwksResolver fetches JWKS endpoints for external issuers. It is shared by every + // external issuer so that they all fetch under the same configured constraints. + jwksResolver *HardenedJwksResolver + env Env } @@ -64,6 +68,7 @@ func NewTokenIssuerCache(env Env) *TokenIssuerCache { env: env, externalIssuers: cmap.New[common.TokenIssuer](), controllerIssuers: cmap.New[common.TokenIssuer](), + jwksResolver: NewHardenedJwksResolver(JwksFetchConfig(env)), } env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtCreate, boltz.EntityCreatedAsync) @@ -172,7 +177,7 @@ func (a *TokenIssuerCache) onExtJwtCreate(signer *db.ExternalJwtSigner) { signerRec := &TokenIssuerExtJwt{ externalJwtSigner: signer, - jwksResolver: &jwks.HttpResolver{}, + jwksResolver: a.jwksResolver, kidToPubKey: map[string]common.IssuerPublicKey{}, } @@ -219,6 +224,21 @@ func (a *TokenIssuerCache) onExtJwtDelete(signer *db.ExternalJwtSigner) { a.externalIssuers.Remove(*signer.Issuer) } +// reportBlockedJwksEndpoint logs an existing external JWT signer whose jwksEndpoint the current +// [edge.externalJwtSigners.jwksFetch] configuration refuses. A configuration change can orphan a +// signer that was created while its endpoint was still permitted, so this is reported at startup +// rather than only when a fetch is attempted. Endpoints that resolve to a blocked address are not +// visible here, as no name resolution is done; those are reported by the fetch itself. +func (a *TokenIssuerCache) reportBlockedJwksEndpoint(signer *db.ExternalJwtSigner) { + if err := checkJwksEndpointAllowed(a.jwksResolver.policy, signer); err != nil { + pfxlog.Logger().WithFields(map[string]interface{}{ + "id": signer.Id, + "name": signer.Name, + "jwksEndpoint": *signer.JwksEndpoint, + }).WithError(err).Error("external jwt signer jwks endpoint is not permitted by the current jwks fetch configuration, its keys cannot be resolved and authentication with this signer will fail") + } +} + // loadExisting loads all external JWT signers and controllers during initialization. func (a *TokenIssuerCache) loadExisting() { err := a.env.GetDb().View(func(tx *bbolt.Tx) error { @@ -235,6 +255,8 @@ func (a *TokenIssuerCache) loadExisting() { continue } + a.reportBlockedJwksEndpoint(signer) + a.onExtJwtCreate(signer) } @@ -485,15 +507,30 @@ func (a *TokenIssuerCache) IterateControllerIssuers(f func(issuer common.TokenIs }) } -// GetIssuerByKid searches both external JWT signers and controller issuers for the one -// that owns the given key ID. Returns nil if no issuer claims that kid. +// GetIssuerByKid searches enabled external JWT signers and then controller issuers for the one +// that owns the given key ID. Disabled external signers are skipped. Returns nil if no issuer +// claims that kid. +// +// External signers can share a kid when they draw from a common signing-key pool, so an external +// match is ambiguous and does not identify a token's issuer. Callers needing a definitive binding +// must resolve by issuer claim instead. func (a *TokenIssuerCache) GetIssuerByKid(kid string) common.TokenIssuer { for _, issuer := range a.externalIssuers.Items() { + if !issuer.IsEnabled() { + continue + } if pubKey, ok := issuer.PubKeyByKid(kid); ok && pubKey.PubKey != nil { return issuer } } + return a.GetControllerIssuerByKid(kid) +} + +// GetControllerIssuerByKid returns the controller TokenIssuer that owns the given key ID, or nil if +// no controller issuer claims that kid. A controller issuer's key ID is the fingerprint of its TLS +// certificate, so this resolves controller-issued tokens by kid without consulting external signers. +func (a *TokenIssuerCache) GetControllerIssuerByKid(kid string) common.TokenIssuer { for _, controller := range a.controllerIssuers.Items() { if pubKey, ok := controller.PubKeyByKid(kid); ok && pubKey.PubKey != nil { return controller @@ -619,6 +656,10 @@ func (r *TokenIssuerExtJwt) VerifyToken(token string) *common.TokenVerificationR return result } + if result.Error = r.verifyIssuerAndAudience(claims); result.Error != nil { + return result + } + result.IdClaimValue, result.Error = resolveStringClaimSelector(claims, r.IdentityIdClaimsSelector()) if result.Error != nil { @@ -628,8 +669,47 @@ func (r *TokenIssuerExtJwt) VerifyToken(token string) *common.TokenVerificationR return result } +// verifyIssuerAndAudience confirms the token's issuer and audience claims match this signer's +// configured values. Signature verification alone resolves the signing key by kid, which is not +// sufficient: a signer that shares keys across audiences (for example a common JWKS) would +// otherwise accept a validly-signed token minted for a different audience or issuer. Callers must +// invoke this only after the signature has been verified. +func (r *TokenIssuerExtJwt) verifyIssuerAndAudience(claims jwt.MapClaims) error { + issuer, err := claims.GetIssuer() + + if err != nil { + return fmt.Errorf("could not retrieve issuer claim from token: %w", err) + } + + if issuer == "" { + return errors.New("token claims did not contain an issuer") + } + + if issuer != r.ExpectedIssuer() { + return fmt.Errorf("token issuer [%s] does not match expected issuer [%s]", issuer, r.ExpectedIssuer()) + } + + audiences, err := claims.GetAudience() + + if err != nil { + return fmt.Errorf("could not retrieve audience claim from token: %w", err) + } + + if len(audiences) == 0 { + return errors.New("token claims did not contain an audience") + } + + if !stringz.Contains(audiences, r.ExpectedAudience()) { + return fmt.Errorf("token audience %v does not match expected audience [%s]", audiences, r.ExpectedAudience()) + } + + return nil +} + // resolveStringSliceClaimProperty extracts a string or string array from JWT claims using a JSON pointer. -// Returns a string slice even if the claim is a single string value. +// Returns a string slice even if the claim is a single string value. An unset selector, a pointer that does +// not resolve against the claims, and a null claim all resolve to no values without error. A claim that is +// present but neither a string nor an array of strings errors. func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]string, error) { if property == "" { return nil, nil @@ -648,7 +728,19 @@ func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]s val, _, err := jsonPointer.Get(claims) if err != nil { - return nil, fmt.Errorf("could not resolve json pointer: %s: %w", property, err) + // The role attributes claim is optional, so a pointer that does not resolve against this + // token's claims yields no attributes rather than an error, matching the unset-selector and + // empty-claim cases. This covers an absent key as well as a traversal failure, such as + // indexing into a scalar. + pfxlog.Logger().WithError(err).WithField("selector", property). + Debug("attribute claim selector did not resolve, enrolling with no role attributes") + return nil, nil + } + + if val == nil { + pfxlog.Logger().WithField("selector", property). + Warn("attribute claim selector resolved to a null claim, enrolling with no role attributes") + return nil, nil } strVal, ok := val.(string) @@ -663,7 +755,7 @@ func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]s arrVals, ok := val.([]any) if !ok { - return nil, fmt.Errorf("could not resolve json pointer: %s: value is not a string or an array of strings, got: %v", property, arrVals) + return nil, fmt.Errorf("could not resolve json pointer: %s: value is not a string or an array of strings, got: %v", property, val) } var attributes []string diff --git a/controller/model/token_provider_cache_test.go b/controller/model/token_provider_cache_test.go new file mode 100644 index 000000000..7f63f02df --- /dev/null +++ b/controller/model/token_provider_cache_test.go @@ -0,0 +1,216 @@ +package model + +import ( + "crypto/x509" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/storage/boltz" + "github.com/stretchr/testify/require" +) + +// Test_TokenIssuerExtJwt_VerifyToken_EnforcesIssuerAndAudience verifies that a validly-signed +// token minted for a different issuer or audience is rejected. The by-inspection enrollment path +// already enforces this; VerifyToken (used by the by-issuer-id enrollment path) must not be weaker. +func Test_TokenIssuerExtJwt_VerifyToken_EnforcesIssuerAndAudience(t *testing.T) { + req := require.New(t) + + testRootCa := newRootCa() + leafKeyPair := testRootCa.NewLeafWithAKID() + + jwksEndpoint := "https://example.com/.well-known/jwks" + + jwksResolver, err := newTestJwksResolver() + req.NoError(err) + + leafKey, err := newKey(leafKeyPair.cert, []*x509.Certificate{leafKeyPair.cert, testRootCa.cert}) + req.NoError(err) + + jwksResolver.AddKey(leafKey, leafKeyPair.key) + + expectedIssuer := "https://idp.example.com" + expectedAudience := "ziti-controller" + + signerRec := &TokenIssuerExtJwt{ + kidToPubKey: map[string]common.IssuerPublicKey{}, + externalJwtSigner: &db.ExternalJwtSigner{ + BaseExtEntity: boltz.BaseExtEntity{ + Id: "fake-id", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + Name: "test-signer", + JwksEndpoint: &jwksEndpoint, + Issuer: &expectedIssuer, + Audience: &expectedAudience, + Enabled: true, + }, + jwksResolver: jwksResolver, + } + + req.NoError(signerRec.Resolve(false)) + + sign := func(claims jwt.MapClaims) string { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = leafKey.KeyId + signed, err := token.SignedString(leafKeyPair.key) + req.NoError(err) + return signed + } + + t.Run("accepts a token with matching issuer and audience", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": expectedIssuer, + "aud": expectedAudience, + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.Truef(result.IsValid(), "token with matching issuer/audience must be accepted: %v", result.Error) + }) + + t.Run("rejects a validly-signed token with a foreign audience", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": expectedIssuer, + "aud": "some-other-audience", + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.False(result.IsValid(), "token minted for a foreign audience must be rejected") + }) + + t.Run("rejects a validly-signed token with a foreign issuer", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": "https://attacker.example.com", + "aud": expectedAudience, + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.False(result.IsValid(), "token minted by a foreign issuer must be rejected") + }) + + t.Run("rejects a token missing the audience claim", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": expectedIssuer, + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.False(result.IsValid(), "token without an audience claim must be rejected") + }) +} + +func Test_resolveStringSliceClaimProperty(t *testing.T) { + t.Run("returns empty when the selector is unset", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": "admin"}, "") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when the claim is absent at the selected path", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"name": "bob"}, "/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when a nested claim is absent at the selected path", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"resource_access": map[string]any{"other": "x"}} + + vals, err := resolveStringSliceClaimProperty(claims, "/resource_access/ziti/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when the claim is present but null", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": nil}, "/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when a nested claim is present but null", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"resource_access": map[string]any{"ziti": map[string]any{"roles": nil}}} + + vals, err := resolveStringSliceClaimProperty(claims, "/resource_access/ziti/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when the claim is present but an empty string", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": ""}, "/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns a single value when the claim is a string", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": "admin"}, "/roles") + + req.NoError(err) + req.Equal([]string{"admin"}, vals) + }) + + t.Run("returns all values when the claim is a string array", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"roles": []any{"admin", "support"}} + + vals, err := resolveStringSliceClaimProperty(claims, "/roles") + + req.NoError(err) + req.Equal([]string{"admin", "support"}, vals) + }) + + t.Run("resolves a nested claim that is present", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"resource_access": map[string]any{"ziti": map[string]any{"roles": []any{"admin"}}}} + + vals, err := resolveStringSliceClaimProperty(claims, "/resource_access/ziti/roles") + + req.NoError(err) + req.Equal([]string{"admin"}, vals) + }) + + t.Run("errors when the claim is present but not a string or array of strings", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"roles": map[string]any{"unexpected": "object"}} + + _, err := resolveStringSliceClaimProperty(claims, "/roles") + + req.Error(err) + req.ErrorContains(err, "map[unexpected:object]") + }) +} diff --git a/controller/network/link_validation_test.go b/controller/network/link_validation_test.go new file mode 100644 index 000000000..a45770729 --- /dev/null +++ b/controller/network/link_validation_test.go @@ -0,0 +1,99 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "testing" + + "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/inspect" + "github.com/openziti/ziti/v2/common/pb/mgmt_pb" + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// newDbLoadedSrcLink builds a link whose source is a router instance loaded from the database. Such an +// instance carries no version, since a router's version arrives in its hello and is not persisted. +func newDbLoadedSrcLink(t *testing.T, srcId string) *model.Link { + t.Helper() + src := &model.Router{} + src.Id = srcId + require.Nil(t, src.VersionInfo, "a database-loaded router carries no version") + + return &model.Link{Id: "l1", DstId: "dst", Src: src} +} + +// TestCheckLinkConns_UnknownVersionIsSkipped: with no connected instance there is no way to know what the +// router reports, so the comparison is skipped rather than failed, matching how a router too old to report +// conn info is treated. +func TestCheckLinkConns_UnknownVersionIsSkipped(t *testing.T) { + _, network, _ := newConnectTestNetwork(t) + + link := newDbLoadedSrcLink(t, "r1") + + result := &mgmt_pb.RouterLinkDetail{IsValid: true} + network.checkLinkConns(link, &inspect.LinkInspectDetail{}, result) + + require.True(t, result.IsValid, "an unknown router version must not make a link invalid") + require.Empty(t, result.Messages) +} + +// TestCheckLinkConns_UsesConnectedVersion: the version lives on the connected instance, so reading it off +// whatever instance the link references reports a link as invalid whenever that instance is one loaded from +// the database rather than the connected one. +func TestCheckLinkConns_UsesConnectedVersion(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + link := newDbLoadedSrcLink(t, "r1") + + connected := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + connected.VersionInfo = &versions.VersionInfo{Version: "v1.0.0"} + network.Router.MarkConnected(connected) + + result := &mgmt_pb.RouterLinkDetail{IsValid: true} + network.checkLinkConns(link, &inspect.LinkInspectDetail{}, result) + + require.True(t, result.IsValid, "the connected instance's version must be what decides the comparison") + require.Empty(t, result.Messages) +} + +// TestCheckLinkConns_ComparesWhenVersionKnown: once the version is known and high enough, the conn info +// comparison proceeds, so skipping above does not quietly disable the check. +func TestCheckLinkConns_ComparesWhenVersionKnown(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + connected := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + connected.VersionInfo = &versions.VersionInfo{Version: "v1.6.6"} + network.Router.MarkConnected(connected) + + link := &model.Link{Id: "l1", DstId: "dst", Src: connected} + + // The router reports a connection the controller does not know about, which the comparison must catch. + routerLink := &inspect.LinkInspectDetail{ + Connections: []*inspect.LinkConnection{ + {Type: "default", Source: "127.0.0.1:1000", Dest: "127.0.0.1:2000"}, + }, + } + + result := &mgmt_pb.RouterLinkDetail{IsValid: true} + network.checkLinkConns(link, routerLink, result) + + // A conn count mismatch reports a message without marking the link invalid, so the message is what + // shows the comparison ran rather than being skipped. + require.NotEmpty(t, result.Messages, "a known version must let the conn info comparison run") + require.Contains(t, result.Messages[0], "len(ctrlConns)") +} diff --git a/controller/network/main_test.go b/controller/network/main_test.go new file mode 100644 index 000000000..cc300cab5 --- /dev/null +++ b/controller/network/main_test.go @@ -0,0 +1,35 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "os" + "testing" + + "github.com/michaelquigley/pfxlog" + "github.com/sirupsen/logrus" +) + +// TestMain configures logging once for the whole package, keeping the path-finding perf tests quiet. +// GlobalInit writes package-level logger state, so calling it from inside a test races any goroutine a +// previous test left running that logs while shutting down: a TestContext's api-session heartbeat +// collector, for instance, does a final flush after its close notification, and that flush logs. Doing +// this before any test starts leaves no concurrent reader to race with. +func TestMain(m *testing.M) { + pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) + os.Exit(m.Run()) +} diff --git a/controller/network/network.go b/controller/network/network.go index 267b30c39..bf00c51f5 100644 --- a/controller/network/network.go +++ b/controller/network/network.go @@ -33,6 +33,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4/protobufs" + "github.com/openziti/channel/v4" "github.com/openziti/foundation/v2/concurrenz" "github.com/openziti/foundation/v2/debugz" "github.com/openziti/foundation/v2/goroutines" @@ -190,6 +191,7 @@ func (self *Network) decodeSyncSnapshotCommand(_ int32, data []byte) (command.Co cmd := &command.SyncSnapshotCommand{ TimelineId: msg.SnapshotId, Snapshot: msg.Snapshot, + ClusterId: msg.ClusterId, SnapshotSink: self.RestoreSnapshot, } @@ -264,9 +266,11 @@ func (network *Network) GetConnectedRouter(routerId string) *model.Router { return network.Router.GetConnected(routerId) } -func (network *Network) GetReloadedRouter(routerId string) (*model.Router, error) { - network.Router.RemoveFromCache(routerId) - return network.Router.Read(routerId) +// NewCtrlChanRouter returns the Router instance representing a new control-channel connection, with the +// channel recorded on it. Each connection gets its own instance, which is what lets the connect and +// disconnect paths tell two racing connections for one router apart. +func (network *Network) NewCtrlChanRouter(ch channel.Channel) (*model.Router, error) { + return network.Router.NewCtrlChanRouter(ch) } func (network *Network) GetRouter(routerId string) (*model.Router, error) { @@ -347,7 +351,47 @@ func (network *Network) ConnectedRouter(id string) bool { return network.Router.IsConnected(id) } -func (network *Network) ConnectRouter(r *model.Router) { +var ( + // ErrConnectRejected indicates a router connect was rejected because another connection for the same + // router is already current. It is returned by ConnectRouter and propagated out of the bind handler so + // NewChannel closes the rejected connection's underlay without starting rx or registering it; the + // router then redials. + ErrConnectRejected = errors.New("router connect rejected: another connection is already current") + + // ErrConnectChannelClosed indicates a router connect was refused because its control channel was + // already closed by the time the connect decision was made, so the connection must not be registered. + ErrConnectChannelClosed = errors.New("router connect rejected: control channel already closed") +) + +// IsConnectRejected reports whether err is (or wraps) a connect refusal that the router recovers from by +// redialing, so the accept path can log it at info rather than treating it as a bind failure. +func IsConnectRejected(err error) bool { + return errors.Is(err, ErrConnectRejected) || errors.Is(err, ErrConnectChannelClosed) +} + +// ConnectRouter registers r as the current connection for its router id, serialized per router. If the +// slot is already held by a different connection it rejects this one (returning ErrConnectRejected) and +// displaces the occupant so its teardown runs; the router redials into the freed slot. A connection whose +// channel is already closed is refused outright (ErrConnectChannelClosed) rather than registered. There is +// at most one connection per router in the connected map at a time. +func (network *Network) ConnectRouter(r *model.Router) error { + unlock := network.Router.LockConnectFor(r.Id) + defer unlock() // leak-safety net; idempotent, so the explicit unlocks below are the ones that matter + + if cur := network.Router.GetConnected(r.Id); cur != nil && cur != r { + // Displace the occupant outside the lock: the teardown acquires the stripe itself (we have + // released it), so there is no reentrant self-deadlock. + unlock() + network.displaceConnection(cur) + return ErrConnectRejected + } + + // Its close handler has already run and never fires again, so nothing would remove it from the + // connected map and every redial would bounce off a slot that can never be freed. + if r.Control == nil || r.Control.IsClosed() { + return ErrConnectChannelClosed + } + network.Link.BuildRouterLinks(r) network.Router.MarkConnected(r) @@ -358,7 +402,10 @@ func (network *Network) ConnectRouter(r *model.Router) { go h.RouterConnected(r) } } + unlock() + go network.ValidateTerminators(r) + return nil } func (network *Network) ValidateTerminators(r *model.Router) { @@ -461,9 +508,59 @@ func (n *Network) ValidateRouterErtTerminators(filter string, cb ErtTerminatorVa return int64(len(result.Entities)), evalF, nil } +// isCurrentConnection reports whether r is still the router's current, connected connection, by pointer +// identity against the connected map (mirrors the check in NotifyExistingLink). A stale or superseded +// connection returns false. +func (network *Network) isCurrentConnection(r *model.Router) bool { + return network.Router.GetConnected(r.Id) == r && r.Connected.Load() +} + +// displaceConnection removes cur, the connection occupying its router's connected slot, so that a redial +// can take the slot. Closing the channel is not sufficient on its own: if it is already closed, its close +// handler has already run and will never run again, so nothing would remove cur and every subsequent +// connect would be rejected against a slot that can never be freed. The teardown is therefore also +// invoked directly; it is gated on connection currency, so it is a no-op once the close handler has +// cleared the slot. Must be called with the router's connect stripe released, since the teardown +// acquires it. +func (network *Network) displaceConnection(cur *model.Router) { + if ch := cur.Control; ch != nil && !ch.IsClosed() { + if err := ch.Close(); err != nil { + pfxlog.Logger().WithError(err).WithField("routerId", cur.Id). + Error("error closing superseded control channel while rejecting connect") + } + } + network.DisconnectRouter(cur) +} + func (network *Network) DisconnectRouter(r *model.Router) { - // 1: remove Links for Router - for _, l := range r.GetLinks() { + // Lock-free pre-check: a stale/superseded disconnect (e.g. the old connection after a takeover) has + // nothing to tear down and must not touch the live connection's state; bail without blocking. + if !network.isCurrentConnection(r) { + return + } + + unlock := network.Router.LockConnectFor(r.Id) + defer unlock() + + // Re-check under the stripe: a newer connection may have taken over between the pre-check and the + // lock. The teardown is all-or-nothing and must not run against a superseded connection. + if !network.isCurrentConnection(r) { + return + } + + // Snapshot the router's links before marking it disconnected: MarkDisconnected clears the + // router's link set (routerLinks.Clear()), so a later r.GetLinks() would return nothing and + // the link-removal/reroute cascade below would be skipped entirely. + links := r.GetLinks() + + // Mark the router disconnected before the RerouteLink cascade, so reroute and everything it + // calls (shortestPath, connected-map reads) sees the dying router as gone. Otherwise reroute + // runs while the router still appears connected and can compute a replacement path through the + // very router that is being removed. + network.Router.MarkDisconnected(r) + + // remove Links for Router, rerouting circuits off any that were connected + for _, l := range links { wasConnected := l.CurrentState().Mode == model.Connected if l.Src.Id == r.Id { network.Link.Remove(l) @@ -472,8 +569,6 @@ func (network *Network) DisconnectRouter(r *model.Router) { network.RerouteLink(l) } } - // 2: remove Router - network.Router.MarkDisconnected(r) for _, h := range network.routerPresenceHandlers.Value() { h.RouterDisconnected(r) @@ -487,23 +582,33 @@ func (network *Network) NotifyExistingLink(srcRouter *model.Router, reportedLink WithField("destRouterId", reportedLink.DestRouterId). WithField("iteration", reportedLink.Iteration) + // Publish under the stripe DisconnectRouter holds: checking currency and then publishing without it + // lets a report recreate a link after the teardown has snapshotted and cleared it. Events go out after + // the unlock, since a dispatcher may be slow and this stripe is shared with connect and disconnect. + unlock := network.Router.LockConnectFor(srcRouter.Id) + src := network.Router.GetConnected(srcRouter.Id) if src == nil { + unlock() log.Info("ignoring links message processed after router disconnected") return } if src != srcRouter || !srcRouter.Connected.Load() { + unlock() log.Info("ignoring links message processed from old router connection") return } dst := network.Router.GetConnected(reportedLink.DestRouterId) + link, created := network.Link.RouterReportedLink(reportedLink, src, dst) + + unlock() + if dst == nil { network.NotifyLinkIdEvent(reportedLink.Id, event.LinkFromRouterDisconnectedDest) } - link, created := network.Link.RouterReportedLink(reportedLink, src, dst) if created { network.NotifyLinkEvent(link, event.LinkFromRouterNew) log.Info("router reported link added") @@ -1360,13 +1465,16 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index } if currentTimelineId != "" && currentTimelineId == cmd.TimelineId { log.WithField("timelineId", cmd.TimelineId).Info("snapshot already current, skipping reload") + // DB already restored; ensure cluster id then raft index (index last, see main path). + if err = network.ensureClusterId(cmd.ClusterId); err != nil { + return fmt.Errorf("failed to set cluster id for already-current snapshot (%w)", err) + } + if err = network.ensureRaftIndex(index); err != nil { + return fmt.Errorf("failed to set raft index for already-current snapshot (%w)", err) + } return nil } - if err != nil { - log.WithError(err).Error("unable to read current raft index before DB restore") - } - buf := bytes.NewBuffer(cmd.Snapshot) reader, err := gzip.NewReader(buf) if err != nil { @@ -1374,14 +1482,15 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index } network.GetDb().RestoreFromReader(reader) - err = network.GetDb().Update(nil, func(ctx boltz.MutateContext) error { - raftBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.MetadataBucket) - raftBucket.SetInt64(db.FieldRaftIndex, int64(index), nil) - return nil - }) - if err != nil { - log.WithError(err).Errorf("failed to set index after restore") + // Write the cluster id before the raft index. The index is the completion gate on restart (the + // FSM skips entries at or below the stored index), so persist it last: any earlier failure then + // replays and retries instead of skipping the command with a blank cluster id. + if err = network.ensureClusterId(cmd.ClusterId); err != nil { + return fmt.Errorf("failed to set cluster id after db restore (%w)", err) + } + if err = network.ensureRaftIndex(index); err != nil { + return fmt.Errorf("failed to set raft index after db restore (%w)", err) } time.AfterFunc(5*time.Second, func() { @@ -1392,6 +1501,29 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index return nil } +// ensureClusterId writes the cluster id if one is not already set (no-op when empty or matching). +// The snapshot restore replaces the whole db, which carries no cluster id, so it must be set here. +func (network *Network) ensureClusterId(clusterId string) error { + if clusterId == "" { + return nil + } + _, err := db.InitClusterId(network.GetDb(), nil, clusterId) + return err +} + +// ensureRaftIndex records the raft index if the stored one is behind it, returning an error on +// failure so a missed index update surfaces rather than being treated as success. +func (network *Network) ensureRaftIndex(index uint64) error { + return network.GetDb().Update(nil, func(ctx boltz.MutateContext) error { + if db.LoadCurrentRaftIndex(ctx.Tx()) >= index { + return nil + } + raftBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.MetadataBucket) + raftBucket.SetInt64(db.FieldRaftIndex, int64(index), nil) + return raftBucket.GetError() + }) +} + func (network *Network) AddInspectTarget(target InspectTarget) { network.inspectionTargets.Append(target) } @@ -1516,9 +1648,14 @@ func (network *Network) checkLinkConns(ctrlLink *model.Link, routerLink *inspect }) } - // ensure that conn info is being reported + // The version comes from the hello, so only the connected instance has it. Not knowing it means the + // comparison cannot be made, which is not a fault of the link. if srcR := ctrlLink.Src; srcR != nil { - hasMinVersion, err := srcR.VersionInfo.HasMinimumVersion("v1.6.6") + connectedSrc := network.Router.GetConnected(srcR.Id) + if connectedSrc == nil || connectedSrc.VersionInfo == nil { + return + } + hasMinVersion, err := connectedSrc.VersionInfo.HasMinimumVersion("v1.6.6") if err != nil { result.IsValid = false result.Messages = append(result.Messages, err.Error()) diff --git a/controller/network/network_path.go b/controller/network/network_path.go index e7df0ee46..ff88abb12 100644 --- a/controller/network/network_path.go +++ b/controller/network/network_path.go @@ -144,6 +144,16 @@ func (network *Network) shortestPath(srcR *model.Router, dstR *model.Router) ([] return nil, 0, errors.New("not routable (!srcR||!dstR)") } + // The graph below is pointer-keyed, so an endpoint held as a different instance of the same router is + // not a node in it and the search reports the router unroutable from itself. Callers legitimately hold + // other instances: each connection has its own, and the router cache holds a database-loaded one. + if connected := network.Router.GetConnected(srcR.Id); connected != nil { + srcR = connected + } + if connected := network.Router.GetConnected(dstR.Id); connected != nil { + dstR = connected + } + if srcR == dstR { return []*model.Router{srcR}, 0, nil } diff --git a/controller/network/route_perf_test.go b/controller/network/route_perf_test.go index 1b8dfecd4..2deddca44 100644 --- a/controller/network/route_perf_test.go +++ b/controller/network/route_perf_test.go @@ -22,14 +22,9 @@ import ( "testing" "github.com/openziti/ziti/v2/controller/model" - - "github.com/michaelquigley/pfxlog" - "github.com/sirupsen/logrus" ) func TestShortestPathAgainstEstablished(t *testing.T) { - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) - ctx := model.NewTestContext(t) defer ctx.Cleanup() @@ -151,7 +146,6 @@ func TestShortestPathAgainstEstablished(t *testing.T) { func BenchmarkShortestPathPerfWithRouterChanges(b *testing.B) { b.StopTimer() - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) ctx := model.NewTestContext(b) defer ctx.Cleanup() @@ -242,7 +236,6 @@ type expectedRoute struct { func BenchmarkShortestPathPerf(b *testing.B) { b.StopTimer() - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) ctx := model.NewTestContext(b) defer ctx.Cleanup() @@ -314,7 +307,6 @@ func BenchmarkShortestPathPerf(b *testing.B) { func BenchmarkMoreRealisticShortestPathPerf(b *testing.B) { //b.StopTimer() - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) ctx := model.NewTestContext(b) defer ctx.Cleanup() diff --git a/controller/network/router_connect_test.go b/controller/network/router_connect_test.go new file mode 100644 index 000000000..f4560c732 --- /dev/null +++ b/controller/network/router_connect_test.go @@ -0,0 +1,383 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "fmt" + "sync" + "sync/atomic" + "testing" + + "github.com/openziti/channel/v4" + "github.com/openziti/transport/v2" + "github.com/openziti/transport/v2/tcp" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/openziti/ziti/v2/common/pb/ctrl_pb" + "github.com/openziti/ziti/v2/controller/change" + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// fakeCtrlChannel is a minimal ctrlchan.CtrlChannel double for connect/disconnect lifecycle tests. +// It embeds the interface (unimplemented methods panic if called), and implements only the close-related +// methods the reject/kick path exercises. onClose, if set, runs synchronously on the first Close to +// simulate the real channel close handler firing DisconnectRouter. +type fakeCtrlChannel struct { + ctrlchan.CtrlChannel + closed atomic.Bool + onClose func() +} + +func (f *fakeCtrlChannel) Close() error { + if f.closed.CompareAndSwap(false, true) && f.onClose != nil { + f.onClose() + } + return nil +} + +func (f *fakeCtrlChannel) IsClosed() bool { return f.closed.Load() } + +func (f *fakeCtrlChannel) IsConnected() bool { return !f.closed.Load() } + +func newConnectTestNetwork(t *testing.T) (*model.TestContext, *Network, transport.Address) { + ctx := model.NewTestContext(t) + t.Cleanup(ctx.Cleanup) + + config := newTestConfig(ctx) + t.Cleanup(func() { close(config.closeNotify) }) + + network, err := NewNetwork(config, ctx) + require.NoError(t, err) + + addr, err := tcp.AddressParser{}.Parse("tcp:0.0.0.0:0") + require.NoError(t, err) + + return ctx, network, addr +} + +// TestConnectRouter_RejectsAndKicksWhenBusy: a connect into an occupied slot returns ErrConnectRejected, +// kicks the occupant (whose teardown clears the slot), and a subsequent redial then connects cleanly. +func TestConnectRouter_RejectsAndKicksWhenBusy(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + currentCh := &fakeCtrlChannel{} + cur := model.NewRouterForTest("r1", "", addr, currentCh, 0, false) + // Simulate the real close handler: closing the occupant runs its DisconnectRouter. + currentCh.onClose = func() { network.DisconnectRouter(cur) } + + require.NoError(t, network.ConnectRouter(cur)) + require.Equal(t, cur, network.Router.GetConnected("r1")) + + newCh := &fakeCtrlChannel{} + rNew := model.NewRouterForTest("r1", "", addr, newCh, 0, false) + err := network.ConnectRouter(rNew) + require.ErrorIs(t, err, ErrConnectRejected) + require.True(t, IsConnectRejected(err)) + require.True(t, currentCh.IsClosed(), "occupant should have been kicked") + require.Nil(t, network.Router.GetConnected("r1"), "kicked occupant's teardown should clear the slot") + require.False(t, rNew.Connected.Load(), "rejected connect must not be registered") + + // Redial into the now-clear slot succeeds. + rRedial := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rRedial)) + require.Equal(t, rRedial, network.Router.GetConnected("r1")) + require.True(t, rRedial.Connected.Load()) +} + +// TestConnectRouter_SetsUpWhenClear: a connect into an empty slot registers the router. +func TestConnectRouter_SetsUpWhenClear(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(r)) + require.Equal(t, r, network.Router.GetConnected("r1")) + require.True(t, r.Connected.Load()) +} + +// TestDisconnectRouter_IgnoresStaleConnection: a disconnect for a superseded connection must not disturb +// the current one (the §9 race guard). +func TestDisconnectRouter_IgnoresStaleConnection(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + rNew := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rNew)) + require.Equal(t, rNew, network.Router.GetConnected("r1")) + + // A stale disconnect for a different (old) instance of the same router id. + rOld := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + network.DisconnectRouter(rOld) + + require.Equal(t, rNew, network.Router.GetConnected("r1"), "stale disconnect must not evict the current connection") + require.True(t, rNew.Connected.Load()) +} + +// TestDisconnectRouter_CurrentTearsDown: a disconnect for the current connection clears it. +func TestDisconnectRouter_CurrentTearsDown(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(r)) + require.Equal(t, r, network.Router.GetConnected("r1")) + + network.DisconnectRouter(r) + require.Nil(t, network.Router.GetConnected("r1")) + require.False(t, r.Connected.Load()) +} + +// TestReject_DisplacesOccupantThatCannotTearItselfDown is the regression guard against a permanent +// reject loop. A connection whose channel closes without its close handler running can never remove +// itself from the connected map, so a reject that merely closed the channel would leave the slot occupied +// forever and every redial would be rejected against a slot nothing can free. The reject must displace +// the occupant itself, so the next redial finds the slot clear. +func TestReject_DisplacesOccupantThatCannotTearItselfDown(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + // No onClose: the occupant's channel goes closed without any teardown running, modeling both a + // channel already closed before the reject and one whose close handler has already run. + currentCh := &fakeCtrlChannel{} + cur := model.NewRouterForTest("r1", "", addr, currentCh, 0, false) + require.NoError(t, network.ConnectRouter(cur)) + require.Equal(t, cur, network.Router.GetConnected("r1")) + currentCh.closed.Store(true) + + rNew := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.ErrorIs(t, network.ConnectRouter(rNew), ErrConnectRejected) + require.Nil(t, network.Router.GetConnected("r1"), "reject must displace an occupant that cannot tear itself down") + require.False(t, cur.Connected.Load()) + require.False(t, rNew.Connected.Load(), "rejected connect must not be registered") + + // The redial therefore makes progress instead of bouncing off the slot forever. + rRedial := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rRedial)) + require.Equal(t, rRedial, network.Router.GetConnected("r1")) + require.True(t, rRedial.Connected.Load()) +} + +// TestConnectRouter_RefusesAlreadyClosedChannel: a connect whose channel is already closed must be +// refused rather than registered. Registering it would put a connection in the connected map that no +// disconnect can ever remove, since its close handler has already run, wedging the router out of this +// controller permanently. +func TestConnectRouter_RefusesAlreadyClosedChannel(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + deadCh := &fakeCtrlChannel{} + deadCh.closed.Store(true) + r := model.NewRouterForTest("r1", "", addr, deadCh, 0, false) + + err := network.ConnectRouter(r) + require.ErrorIs(t, err, ErrConnectChannelClosed) + require.True(t, IsConnectRejected(err), "an already-closed channel is a refusal the router redials after") + require.Nil(t, network.Router.GetConnected("r1"), "a closed connection must not occupy the slot") + require.False(t, r.Connected.Load()) + + // A subsequent healthy connect is unaffected. + rOk := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rOk)) + require.Equal(t, rOk, network.Router.GetConnected("r1")) +} + +// TestConnectDisconnectRace exercises concurrent connect/disconnect for one router id under the race +// detector, asserting the map never ends holding a disconnected router. +func TestConnectDisconnectRace(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + var wg sync.WaitGroup + for i := 0; i < 64; i++ { + wg.Add(1) + go func(connect bool) { + defer wg.Done() + if connect { + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + _ = network.ConnectRouter(r) + } else if cur := network.Router.GetConnected("r1"); cur != nil { + network.DisconnectRouter(cur) + } + }(i%2 == 0) + } + wg.Wait() + + if cur := network.Router.GetConnected("r1"); cur != nil { + require.True(t, cur.Connected.Load(), "map must not hold a disconnected router") + } +} + +// fakeConnectChannel is a channel.MultiChannel double reporting a fixed router id and a real +// ListenerCtrlChannel as its underlay handler, which is what the accept path records on the router. It +// embeds the interface, so any method these tests do not exercise panics rather than returning a zero value. +type fakeConnectChannel struct { + channel.MultiChannel + id string + underlayHandler channel.UnderlayHandler +} + +func (f *fakeConnectChannel) Id() string { return f.id } +func (f *fakeConnectChannel) GetUnderlayHandler() channel.UnderlayHandler { return f.underlayHandler } +func (f *fakeConnectChannel) SetLogicalName(string) {} + +func newFakeConnectChannel(routerId string) *fakeConnectChannel { + ctrlCh := ctrlchan.NewListenerCtrlChannel() + ch := &fakeConnectChannel{id: routerId, underlayHandler: ctrlCh} + // The channel framework hands the ctrl channel its channel on creation; do the same here so the + // double records it the way a real connection would. + ctrlCh.ChannelCreated(ch) + return ch +} + +// newPersistedRouter stores a router so the connect path can load it. Create publishes the instance it was +// given into the router cache, which is the instance a connection must not be handed. +func newPersistedRouter(t *testing.T, network *Network, addr transport.Address, id string) *model.Router { + t.Helper() + router := model.NewRouterForTest(id, "", addr, nil, 0, false) + require.NoError(t, network.Router.Create(router, change.New())) + return router +} + +// TestNewCtrlChanRouter_InstanceIsNotShared is the guard on the assumption every currency check in the +// connect and disconnect paths rests on: each connection gets its own Router instance. Two connections +// sharing one instance are indistinguishable to those checks, so a connect into an occupied slot is not +// rejected and the first connection's teardown dismantles the second's registration, leaving a live +// control channel whose router is not registered and can never re-register. +func TestNewCtrlChanRouter_InstanceIsNotShared(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + cached := newPersistedRouter(t, network, addr, "r1") + + first, err := network.NewCtrlChanRouter(newFakeConnectChannel("r1")) + require.NoError(t, err) + second, err := network.NewCtrlChanRouter(newFakeConnectChannel("r1")) + require.NoError(t, err) + + require.NotSame(t, first, second, "each connection must get its own router instance") + require.NotSame(t, cached, first, "a connection must not be handed the cached instance") + require.NotSame(t, cached, second, "a connection must not be handed the cached instance") + + // The connection's instance must not become the cached one either, or the next connection would be + // handed it. + readBack, err := network.Router.Read("r1") + require.NoError(t, err) + require.NotSame(t, first, readBack, "a connection's instance must stay out of the router cache") + require.NotSame(t, second, readBack, "a connection's instance must stay out of the router cache") +} + +// TestNewCtrlChanRouter_ConcurrentConnectsAreNotShared covers the way instances came to be shared: the +// load was an eviction followed by a read-through read, so two connects racing could both evict and the +// later read could then hit the instance the earlier one had just published. +func TestNewCtrlChanRouter_ConcurrentConnectsAreNotShared(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + newPersistedRouter(t, network, addr, "r1") + + const connects = 16 + start := make(chan struct{}) + results := make([]*model.Router, connects) + var wg sync.WaitGroup + + for i := 0; i < connects; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + <-start + r, err := network.NewCtrlChanRouter(newFakeConnectChannel("r1")) + if err == nil { + results[idx] = r + } + }(i) + } + + close(start) + wg.Wait() + + seen := map[*model.Router]int{} + for idx, r := range results { + require.NotNil(t, r, "connect %d failed to load a router", idx) + seen[r]++ + } + require.Len(t, seen, connects, "every concurrent connect must get its own router instance") +} + +// TestNewCtrlChanRouter_RecordsTheChannel: the instance arrives carrying its connection, so no caller has +// to remember to attach it, and the channel it carries is the one it was built for. +func TestNewCtrlChanRouter_RecordsTheChannel(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + newPersistedRouter(t, network, addr, "r1") + + ch := newFakeConnectChannel("r1") + r, err := network.NewCtrlChanRouter(ch) + require.NoError(t, err) + + require.NotNil(t, r.Control, "the connection's channel must be recorded") + require.Same(t, ch, r.Control.GetChannel(), "the recorded channel must be the one the instance was built for") + require.False(t, r.ConnectTime.IsZero(), "connect time must be recorded") + require.False(t, r.Connected.Load(), "loading a router must not register it as connected") +} + +// TestNewCtrlChanRouter_UnknownRouter: a channel from a router the controller has no record of is refused +// rather than yielding an empty instance. +func TestNewCtrlChanRouter_UnknownRouter(t *testing.T) { + _, network, _ := newConnectTestNetwork(t) + + r, err := network.NewCtrlChanRouter(newFakeConnectChannel("nope")) + require.Error(t, err) + require.Nil(t, r) +} + +// TestNotifyExistingLink_RaceDisconnect is the invariant that link publication and disconnect teardown +// cannot interleave: once a router is disconnected, no link may remain naming it as its source. +// +// Checking currency and then publishing without holding the router's connect stripe is a check-then-act. +// A report can find the connection current, and by the time it reaches the link manager the teardown has +// already taken its snapshot of the router's links and cleared them, so the link is recreated after +// everything that would have removed it has run. It is then invisible to the router (its index was +// cleared) while still in the controller's link table with a disconnected source, and a reconnect +// reporting the same iteration can adopt that stale source rather than rebuilding the link. +// +// Run under -race, and repeated, since the window is small. +func TestNotifyExistingLink_RaceDisconnect(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + // The destination is left unconnected: only the source router's disconnect can recreate a stale link, + // so connecting a second router would add the peer-state sync to the race for no extra coverage. + for i := 0; i < 200; i++ { + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(r)) + + reported := &ctrl_pb.RouterLinks_RouterLink{ + Id: fmt.Sprintf("l-%d", i), + DestRouterId: "r2", + LinkProtocol: "tls", + DialAddress: "tcp:localhost:1234", + Iteration: 1, + } + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + network.NotifyExistingLink(r, reported) + }() + go func() { + defer wg.Done() + network.DisconnectRouter(r) + }() + wg.Wait() + + require.Nil(t, network.Router.GetConnected("r1"), "the router must be disconnected") + for _, l := range network.Link.All() { + require.NotEqual(t, "r1", l.Src.Id, + "iteration %d: a link published by a router that has been disconnected must not survive", i) + } + } +} diff --git a/controller/network/router_messaging.go b/controller/network/router_messaging.go index 22f7a264f..f4d4640e2 100644 --- a/controller/network/router_messaging.go +++ b/controller/network/router_messaging.go @@ -201,6 +201,13 @@ func (self *RouterMessaging) syncStates() { continue } + // Sending on a closed channel fails immediately, so keeping these queued retries as fast as the + // event loop spins. Reconnecting resyncs everything, so discard them. + if ch := notifyRouter.Control; ch == nil || ch.IsClosed() { + delete(self.routerUpdates, k) + continue + } + if v.sendInProgress.Load() { continue } @@ -243,15 +250,15 @@ func (self *RouterMessaging) syncStates() { currentStatesVersion := updates.version queueErr := self.routerCommPool.QueueOrError(func() { - ch := notifyRouter.Control - if ch == nil { - return - } - - success := true - if err := protobufs.MarshalTyped(changes).WithTimeout(time.Second * 1).SendAndWaitForWire(ch.GetDefaultSender()); err != nil { - pfxlog.Logger().WithError(err).WithField("routerId", notifyRouter.Id).Error("failed to send peer state changes to router") - success = false + // The done event must be queued on every path, including a missing channel: it is what clears + // sendInProgress, and without it this router's updates would never be attempted again. + success := false + if ch := notifyRouter.Control; ch != nil { + if err := protobufs.MarshalTyped(changes).WithTimeout(time.Second * 1).SendAndWaitForWire(ch.GetDefaultSender()); err != nil { + pfxlog.Logger().WithError(err).WithField("routerId", notifyRouter.Id).Error("failed to send peer state changes to router") + } else { + success = true + } } self.queueEvent(&routerPeerChangesSendDone{ diff --git a/controller/network/router_messaging_test.go b/controller/network/router_messaging_test.go new file mode 100644 index 000000000..03f727b12 --- /dev/null +++ b/controller/network/router_messaging_test.go @@ -0,0 +1,58 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "testing" + + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// TestSyncStates_DiscardsUpdatesForClosedChannel covers the retry behaviour for peer state changes bound +// for a router whose control channel has closed while it is still in the connected map. Sending on a +// closed channel fails immediately instead of blocking, and a failed send is retried as soon as the event +// loop turns, so retaining the updates spins the loop and floods the log. Reconnecting resyncs everything, +// so the pending changes are discarded instead. Updates for a router with a live channel must still be +// retained. +func TestSyncStates_DiscardsUpdatesForClosedChannel(t *testing.T) { + ctx, network, addr := newConnectTestNetwork(t) + + // A standalone instance, so syncStates can be driven directly; the Network's own RouterMessaging runs + // an event loop goroutine that would race these map accesses. + rm := NewRouterMessaging(ctx, network.RouterMessaging.routerCommPool) + + deadCh := &fakeCtrlChannel{} + dead := model.NewRouterForTest("r1", "", addr, deadCh, 0, false) + network.Router.MarkConnected(dead) + // The channel closes without any teardown running, so it stays in the connected map. + deadCh.closed.Store(true) + rm.routerUpdates["r1"] = &routerUpdates{changedRouters: map[string]struct{}{"other": {}}} + + // A live router with a send already in flight, which syncStates skips without queueing another. + liveCh := &fakeCtrlChannel{} + live := model.NewRouterForTest("r2", "", addr, liveCh, 0, false) + network.Router.MarkConnected(live) + liveUpdates := &routerUpdates{changedRouters: map[string]struct{}{"other": {}}} + liveUpdates.sendInProgress.Store(true) + rm.routerUpdates["r2"] = liveUpdates + + rm.syncStates() + + require.NotContains(t, rm.routerUpdates, "r1", "updates for a closed channel must be discarded, not retried") + require.Contains(t, rm.routerUpdates, "r2", "updates for a live channel must be retained") +} diff --git a/controller/oidc_auth/provider.go b/controller/oidc_auth/provider.go index 8494df99c..3f4c53634 100644 --- a/controller/oidc_auth/provider.go +++ b/controller/oidc_auth/provider.go @@ -24,6 +24,8 @@ import ( "github.com/gorilla/mux" "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/controller/api" + "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/model" "github.com/pkg/errors" @@ -99,6 +101,15 @@ func NewNativeOnlyOP(ctx context.Context, env model.Env, config Config) (http.Ha } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // OIDC endpoints buffer request bodies before authentication, so cap what an + // unauthenticated client can make the controller hold in memory + if r.ContentLength > api.MaxRequestBodySize { + renderJsonApiError(w, apierror.NewRequestEntityTooLarge()) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, api.MaxRequestBodySize) + for iss, handler := range handlers { if err := iss.ValidFor(r.Host); err == nil { handler.ServeHTTP(w, r) diff --git a/controller/oidc_auth/render.go b/controller/oidc_auth/render.go index ea0e707f1..c1a2a0444 100644 --- a/controller/oidc_auth/render.go +++ b/controller/oidc_auth/render.go @@ -24,6 +24,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/edge-api/rest_model" "github.com/openziti/foundation/v2/errorz" + "github.com/openziti/ziti/v2/controller/apierror" ) // render will attempt to send a responses on the provided http.ResponseWriter. All error output will be directed to the @@ -93,7 +94,13 @@ func renderJsonApiError(w http.ResponseWriter, err *errorz.ApiError) { func errorToRestApiError(err error) (*rest_model.APIError, int) { var typedErr *errorz.ApiError + var maxBytesErr *http.MaxBytesError switch { + case errors.As(err, &maxBytesErr): + return &rest_model.APIError{ + Code: apierror.RequestEntityTooLargeCode, + Message: apierror.RequestEntityTooLargeMessage, + }, http.StatusRequestEntityTooLarge case errors.As(err, &typedErr): restErr := &rest_model.APIError{ Code: typedErr.AppCode, diff --git a/controller/oidc_auth/storage.go b/controller/oidc_auth/storage.go index 3d9a4f689..bb4cb54ca 100644 --- a/controller/oidc_auth/storage.go +++ b/controller/oidc_auth/storage.go @@ -455,7 +455,6 @@ func (s *HybridStorage) Authenticate(authCtx model.AuthContext, id string, confi if certAuth != nil { authRequest.IsCertExtendable = certAuth.IsIssuedByNetwork - authRequest.IsCertExtendable = true authRequest.IsCertKeyRollRequested = certAuth.IsKeyRollRequested authRequest.ImproperClientCertChain = result.ImproperClientCertChain() } diff --git a/controller/raft/fsm.go b/controller/raft/fsm.go index 4d14257cb..5bbe3f922 100644 --- a/controller/raft/fsm.go +++ b/controller/raft/fsm.go @@ -30,11 +30,11 @@ import ( "github.com/hashicorp/raft" "github.com/michaelquigley/pfxlog" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/command" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/event" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/sirupsen/logrus" "go.etcd.io/bbolt" bbolterrors "go.etcd.io/bbolt/errors" @@ -306,7 +306,8 @@ func (self *BoltDbFsm) Apply(log *raft.Log) interface{} { return err } - logger.Infof("apply log with type %T", cmd) + logger = logger.WithField("cmdType", fmt.Sprintf("%T", cmd)) + logger.Info("applying log") changeCtx := cmd.GetChangeContext() if changeCtx == nil { changeCtx = change.New().SetSourceType("unattributed").SetChangeAuthorType(change.AuthorTypeUnattributed) @@ -319,8 +320,13 @@ func (self *BoltDbFsm) Apply(log *raft.Log) interface{} { }) if err = cmd.Apply(ctx); err != nil { + if _, critical := cmd.(command.CriticalCommand); critical { + // Base-state command failed; halt instead of advancing over incomplete state. The + // in-tx index update rolled back with the apply, so raft replays it on restart. + logger.WithError(err).Fatal("failed to apply critical base-state command; halting rather than advancing over incomplete state") + } logger.WithError(err).Error("applying log resulted in error") - // if this errored, assume that we haven't updated the index in the db + // apply rolled back the in-tx index update; persist it here since raft advances regardless self.updateIndex(log.Index) } diff --git a/controller/raft/mesh/mesh.go b/controller/raft/mesh/mesh.go index e828d3e89..ddc7495d1 100644 --- a/controller/raft/mesh/mesh.go +++ b/controller/raft/mesh/mesh.go @@ -29,6 +29,7 @@ import ( "github.com/openziti/foundation/v2/concurrenz" "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/cert" "github.com/openziti/ziti/v2/controller/event" "github.com/hashicorp/raft" @@ -42,12 +43,13 @@ import ( const ( // New header IDs, starting at 2000 to avoid conflicts with channel base headers (0-12+) - PeerAddrHeader = 2000 - SigningCertHeader = 2001 - ApiAddressesHeader = 2002 - RaftConnIdHeader = 2003 - ClusterIdHeader = 2004 - PreferredLeaderHeader = 2005 + PeerAddrHeader = 2000 + SigningCertHeader = 2001 + ApiAddressesHeader = 2002 + RaftConnIdHeader = 2003 + ClusterIdHeader = 2004 + PreferredLeaderHeader = 2005 + SigningCertChainHeader = 2006 // full signing cert chain, leaf first, as concatenated DER // Legacy header IDs, used as fallback when reading from older peers LegacyPeerAddrHeader = 11 @@ -70,11 +72,12 @@ type Peer struct { Id raft.ServerID Address string Channel channel.Channel - RaftConns concurrenz.CopyOnWriteMap[uint32, *raftPeerConn] + ClusterId string Version *versions.VersionInfo SigningCerts []*x509.Certificate ApiAddresses map[string][]event.ApiAddress PreferredLeader bool + RaftConns concurrenz.CopyOnWriteMap[uint32, *raftPeerConn] raftPeerIdGen uint32 } @@ -323,6 +326,10 @@ type Mesh interface { RegisterClusterStateHandler(f func(state ClusterState)) Init(bindHandler channel.BindHandler) CleanupDialRecords() + + // RevalidatePeerClusterIds drops connected peers whose cluster id no longer matches the local + // cluster id. It is called when the local node acquires or changes its cluster id. + RevalidatePeerClusterIds() } func New(env Env, raftAddr raft.ServerAddress, helloHeaderProviders []HeaderProvider) Mesh { @@ -452,8 +459,10 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer tlsCert := self.nodeId.ServerCert() var serverCert []byte + var serverCertChain []byte if len(tlsCert) != 0 && len(tlsCert[0].Certificate) != 0 { serverCert = tlsCert[0].Certificate[0] + serverCertChain = ConcatDer(tlsCert[0].Certificate) } headers := map[int32][]byte{ @@ -463,6 +472,7 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer LegacyPeerAddrHeader: []byte(self.raftAddr), SigningCertHeader: serverCert, LegacySigningCertHeader: serverCert, + SigningCertChainHeader: serverCertChain, ClusterIdHeader: []byte(self.env.GetClusterId()), LegacyClusterIdHeader: []byte(self.env.GetClusterId()), } @@ -472,14 +482,16 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer } // Check if a recent dial to this address failed (e.g., hello too large for an old peer). - // If so, strip the new signing cert header so only the legacy header is sent, allowing + // If so, strip the new signing cert headers so only the legacy header is sent, allowing // the connection to succeed with old controllers that enforce the smaller hello limit. self.lock.RLock() if rec := self.dialRecords[address]; rec != nil && time.Since(rec.lastAttempt) < RecentDialInterval { if rec.peerVersion == nil { delete(headers, SigningCertHeader) + delete(headers, SigningCertChainHeader) } else if hasMin, _ := rec.peerVersion.HasMinimumVersion("v2.0.0"); !hasMin { delete(headers, SigningCertHeader) + delete(headers, SigningCertChainHeader) } } self.lock.RUnlock() @@ -514,6 +526,7 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer if err = self.validateConnection(peer.Channel); err != nil { return err } + peer.ClusterId = getPeerClusterId(peer.Channel) underlay := binding.GetChannel().Underlay() id, err := self.extractPeerId(underlay.GetRemoteAddr().String(), underlay.Certificates()) @@ -544,7 +557,10 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer } peer.Version = versionInfo - peer.SigningCerts = []*x509.Certificate{underlay.Certificates()[0]} + peer.SigningCerts = signingCertsFromHeaders(peer.Channel.Underlay().Headers()) + if len(peer.SigningCerts) == 0 { + peer.SigningCerts = underlay.Certificates() + } self.lock.Lock() if rec := self.dialRecords[address]; rec != nil { @@ -589,27 +605,63 @@ func (self *impl) validateConnection(ch channel.Channel) error { } func (self *impl) checkClusterIds(ch channel.Channel) error { - clusterIdBytes, _ := headerWithFallback(ch.Underlay().Headers(), ClusterIdHeader, LegacyClusterIdHeader) - clusterId := string(clusterIdBytes) + clusterId := getPeerClusterId(ch) if clusterId != "" && self.env.GetClusterId() != "" && clusterId != self.env.GetClusterId() { return fmt.Errorf("local cluster id %s doesn't match peer cluster id %s", self.env.GetClusterId(), clusterId) } return nil } -func (self *impl) checkCerts(ch channel.Channel) error { - certs := ch.Underlay().Certificates() - if len(certs) == 0 { - return errors.New("unable to validate peer connection, no certs presented") - } +// getPeerClusterId returns the cluster id the peer advertised, or "" if none (a blank node not yet +// joined or bootstrapped). +func getPeerClusterId(ch channel.Channel) string { + clusterIdBytes, _ := headerWithFallback(ch.Underlay().Headers(), ClusterIdHeader, LegacyClusterIdHeader) + return string(clusterIdBytes) +} - for _, cert := range ch.Underlay().Certificates() { - if _, err := self.env.GetNodeId().CaPool().VerifyToRoot(cert); err == nil { - return nil +// RevalidatePeerClusterIds drops connected peers whose known cluster id differs from the local one. +// The bind-time check runs once and skips empty ids, so a node that connected while blank and later +// acquired a different id would otherwise stay cross-connected. Called when the local id is set. +func (self *impl) RevalidatePeerClusterIds() { + localId := self.env.GetClusterId() + for _, peer := range peersWithMismatchedClusterId(localId, self.GetPeers()) { + pfxlog.Logger(). + WithField("peerId", string(peer.Id)). + WithField("peerAddress", peer.Address). + WithField("peerClusterId", peer.ClusterId). + WithField("localClusterId", localId). + Error("dropping peer connection with mismatched cluster id") + if err := peer.Channel.Close(); err != nil { + pfxlog.Logger().WithError(err). + WithField("peerId", string(peer.Id)). + Error("error closing peer channel with mismatched cluster id") } } +} - return errors.New("unable to validate peer connection, no certs presented matched the CA for this node") +// peersWithMismatchedClusterId returns peers whose known cluster id differs from localId. Peers with +// an empty id (legitimate blank joiners) are never returned, nor is anything when localId is empty. +func peersWithMismatchedClusterId(localId string, peers map[string]*Peer) []*Peer { + if localId == "" { + return nil + } + var mismatched []*Peer + for _, peer := range peers { + if peer.ClusterId != "" && peer.ClusterId != localId { + mismatched = append(mismatched, peer) + } + } + return mismatched +} + +func (self *impl) checkCerts(ch channel.Channel) error { + // Peer identity is taken from certs[0] via ExtractSpiffeId, so certs[0] is the certificate that must + // chain to a trusted CA; VerifyLeafCertChain verifies that leaf specifically against the node's full + // trusted-CA pool. + if _, err := cert.VerifyLeafCertChain(self.env.GetNodeId().CA(), ch.Underlay().Certificates()); err != nil { + return fmt.Errorf("unable to validate peer connection: %w", err) + } + return nil } func (self *impl) GetPeerInfo(address string, timeout time.Duration) (raft.ServerID, raft.ServerAddress, error) { @@ -701,6 +753,12 @@ func ExtractSpiffeId(certs []*x509.Certificate) (string, error) { func (self *impl) PeerConnected(peer *Peer, dial bool) error { self.lock.Lock() + // Re-check the cluster id under the lock: the bind-time check can race a local-id change that + // lands before the peer is registered, which RevalidatePeerClusterIds would then miss. + if localId := self.env.GetClusterId(); localId != "" && peer.ClusterId != "" && peer.ClusterId != localId { + self.lock.Unlock() + return fmt.Errorf("peer %v cluster id %s does not match local cluster id %s", peer.Id, peer.ClusterId, localId) + } if self.Peers[peer.Address] != nil { defer self.lock.Unlock() return fmt.Errorf("connection from peer %v @ %v already present", peer.Id, peer.Address) @@ -879,15 +937,12 @@ func (self *impl) AcceptUnderlay(underlay channel.Underlay) error { if err = self.validateConnection(peer.Channel); err != nil { return err } + peer.ClusterId = getPeerClusterId(peer.Channel) peer.Version = versionInfo - if certHeader, found := headerWithFallback(ch.Underlay().Headers(), SigningCertHeader, LegacySigningCertHeader); found { - if cert, err := x509.ParseCertificate(certHeader); err == nil { - peer.SigningCerts = []*x509.Certificate{cert} - } - } + peer.SigningCerts = signingCertsFromHeaders(ch.Underlay().Headers()) if len(peer.SigningCerts) == 0 { - peer.SigningCerts = []*x509.Certificate{underlay.Certificates()[0]} + peer.SigningCerts = underlay.Certificates() } binding.AddReceiveHandlerF(RaftDataType, peer.handleReceiveData) @@ -974,6 +1029,33 @@ func headerWithFallback(headers map[int32][]byte, key int32, legacyKey int32) ([ return nil, false } +// ConcatDer concatenates DER-encoded certificates into a single byte slice, parseable +// with x509.ParseCertificates. +func ConcatDer(certs [][]byte) []byte { + var result []byte + for _, der := range certs { + result = append(result, der...) + } + return result +} + +// signingCertsFromHeaders extracts a peer's signing certificates from hello headers, +// preferring the full chain header (leaf first) over the older single-cert headers. +// Returns nil when no header yields a certificate. +func signingCertsFromHeaders(headers map[int32][]byte) []*x509.Certificate { + if chainHeader, found := headers[SigningCertChainHeader]; found { + if certs, err := x509.ParseCertificates(chainHeader); err == nil && len(certs) > 0 { + return certs + } + } + if certHeader, found := headerWithFallback(headers, SigningCertHeader, LegacySigningCertHeader); found { + if signingCert, err := x509.ParseCertificate(certHeader); err == nil { + return []*x509.Certificate{signingCert} + } + } + return nil +} + func getUint32HeaderWithFallback(m *channel.Message, key int32, legacyKey int32) (uint32, bool) { if val, ok := m.GetUint32Header(key); ok { return val, true diff --git a/controller/raft/mesh/mesh_peer_cert_validation_test.go b/controller/raft/mesh/mesh_peer_cert_validation_test.go new file mode 100644 index 000000000..d1f9420df --- /dev/null +++ b/controller/raft/mesh/mesh_peer_cert_validation_test.go @@ -0,0 +1,246 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mesh + +// End-to-end validation of the mesh peer certificate check over a real TCP+TLS socket, using the +// production controller TLS config (identity.ServerTLSConfig). The ctrl listener uses +// RequireAnyClientCert and does not verify the client chain, so the handshake completes for any +// presented client leaf; peer admission is enforced afterward by the direction-aware cert-chain check +// against the node CA pool. Unit-level coverage of the shared check lives in common/cert. + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "net/url" + "strings" + "testing" + "time" + + "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/cert" + "github.com/stretchr/testify/require" +) + +const testTrustDomain = "spiffe://mesh-cert-test" + +type certAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var testSerial int64 + +func nextTestSerial() *big.Int { + testSerial++ + return big.NewInt(testSerial) +} + +func mkTestKey(t *testing.T) *ecdsa.PrivateKey { + t.Helper() + k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + return k +} + +func signTestCert(t *testing.T, tmpl, parent *x509.Certificate, pub *ecdsa.PublicKey, signerKey *ecdsa.PrivateKey) *x509.Certificate { + t.Helper() + der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, pub, signerKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return c +} + +func mkCA(t *testing.T, cn string, parent *certAndKey) *certAndKey { + t.Helper() + key := mkTestKey(t) + tmpl := &x509.Certificate{ + SerialNumber: nextTestSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + signParent, signKey := tmpl, key + if parent != nil { + signParent, signKey = parent.cert, parent.key + } + return &certAndKey{cert: signTestCert(t, tmpl, signParent, &key.PublicKey, signKey), key: key} +} + +func mkLeafWithKey(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *certAndKey, key *ecdsa.PrivateKey) *certAndKey { + t.Helper() + tmpl := &x509.Certificate{ + SerialNumber: nextTestSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: ekus, + } + if spiffePath != "" { + u, err := url.Parse(testTrustDomain + spiffePath) + require.NoError(t, err) + tmpl.URIs = []*url.URL{u} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + return &certAndKey{cert: signTestCert(t, tmpl, signParent, &key.PublicKey, signKey), key: key} +} + +func mkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *certAndKey) *certAndKey { + return mkLeafWithKey(t, cn, spiffePath, ekus, signer, mkTestKey(t)) +} + +func pemOfCerts(t *testing.T, certs ...*x509.Certificate) string { + t.Helper() + var b strings.Builder + for _, c := range certs { + require.NoError(t, pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})) + } + return b.String() +} + +func pemOfKey(t *testing.T, key *ecdsa.PrivateKey) string { + t.Helper() + der, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + var b strings.Builder + require.NoError(t, pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: der})) + return b.String() +} + +// Test_MeshPeerCert_LiveHandshake stands up a real TLS listener with the production node identity, +// then connects as both a rogue and a legitimate peer. The rogue presents a self-signed identity leaf +// plus the node's own (scraped) server cert as an extra certificate; the handshake completes, but the +// mesh check must reject it. The legitimate peer presents a CA-signed client leaf and must be accepted. +func Test_MeshPeerCert_LiveHandshake(t *testing.T) { + req := require.New(t) + + root := mkCA(t, "root", nil) + inter := mkCA(t, "int", root) + + // Real node identity: one key backs the client and server certs (LoadIdentity uses the default + // key for the server cert when no server_key is configured). + nodeKey := mkTestKey(t) + clientCert := mkLeafWithKey(t, "node-client", "/controller/real-id", + []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, inter, nodeKey) + serverCert := mkLeafWithKey(t, "node-server", "", + []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter, nodeKey) + + id, err := identity.LoadIdentity(identity.Config{ + Key: "pem:" + pemOfKey(t, nodeKey), + Cert: "pem:" + pemOfCerts(t, clientCert.cert, inter.cert), + ServerCert: "pem:" + pemOfCerts(t, serverCert.cert, inter.cert), + CA: "pem:" + pemOfCerts(t, root.cert, inter.cert), + }) + req.NoError(err) + + serverCfg := id.ServerTLSConfig() + req.NotNil(serverCfg) + req.Equal(tls.RequireAnyClientCert, serverCfg.ClientAuth) + + rawLn, err := net.Listen("tcp", "127.0.0.1:0") + req.NoError(err) + defer func() { _ = rawLn.Close() }() + ln := tls.NewListener(rawLn, serverCfg) + addr := ln.Addr().String() + + type acceptResult struct { + peer []*x509.Certificate + err error + } + results := make(chan acceptResult, 3) + go func() { + for i := 0; i < 3; i++ { + c, aerr := ln.Accept() + if aerr != nil { + results <- acceptResult{err: aerr} + continue + } + tc := c.(*tls.Conn) + _ = tc.SetDeadline(time.Now().Add(10 * time.Second)) + if herr := tc.Handshake(); herr != nil { + results <- acceptResult{err: herr} + _ = tc.Close() + continue + } + results <- acceptResult{peer: tc.ConnectionState().PeerCertificates} + _ = tc.Close() + } + }() + + ca := id.CA() + + // Scrape the node's own server certificate from the listener (a throwaway client cert satisfies + // RequireAnyClientCert). This is the "extra" certificate the rogue peer will present. + throwaway := mkLeaf(t, "throwaway", "", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + scrapeConn, err := tls.Dial("tcp", addr, &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{{Certificate: [][]byte{throwaway.cert.Raw}, PrivateKey: throwaway.key, Leaf: throwaway.cert}}, + }) + req.NoError(err) + serverPresented := scrapeConn.ConnectionState().PeerCertificates + _ = scrapeConn.Close() + req.NotEmpty(serverPresented) + extra := serverPresented[0] + req.Equal("node-server", extra.Subject.CommonName) + <-results + + // The scraped server certificate is server-auth-only, yet it chains to the node CA. Verification is + // EKU-agnostic and anchors on the node's full trusted-CA pool, so it is accepted - this is the + // certificate an outbound dial would present as its leaf. + _, err = cert.VerifyLeafCertChain(ca, serverPresented) + req.NoError(err, "a server-auth leaf that chains to the node CA is accepted") + + // Rogue peer: self-signed identity leaf + the scraped extra cert. Handshake completes; the mesh + // check must reject it because the identity leaf (certs[0]) does not chain to the CA. + rogue := mkLeaf(t, "rogue", "/controller/victim-id", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + rogueConn, err := tls.Dial("tcp", addr, &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{{Certificate: [][]byte{rogue.cert.Raw, extra.Raw}, PrivateKey: rogue.key, Leaf: rogue.cert}}, + }) + req.NoError(err, "handshake completes under RequireAnyClientCert even for a self-signed leaf") + _ = rogueConn.Close() + rogueRes := <-results + req.NoError(rogueRes.err) + _, err = cert.VerifyLeafCertChain(ca, rogueRes.peer) + req.Error(err, "mesh check rejects a self-signed identity leaf backed by a scraped extra cert") + + // Legitimate peer: CA-signed client leaf. Handshake completes and the mesh check accepts it. + legitConn, err := tls.Dial("tcp", addr, &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{{Certificate: [][]byte{clientCert.cert.Raw, inter.cert.Raw}, PrivateKey: nodeKey, Leaf: clientCert.cert}}, + }) + req.NoError(err) + _ = legitConn.Close() + legitRes := <-results + req.NoError(legitRes.err) + _, err = cert.VerifyLeafCertChain(ca, legitRes.peer) + req.NoError(err, "mesh check accepts a legitimate CA-signed peer leaf") +} diff --git a/controller/raft/mesh/mesh_test.go b/controller/raft/mesh/mesh_test.go index 49e46b96b..7b1b65790 100644 --- a/controller/raft/mesh/mesh_test.go +++ b/controller/raft/mesh/mesh_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/openziti/channel/v4" "github.com/openziti/foundation/v2/versions" "github.com/openziti/ziti/v2/controller/event" @@ -56,6 +57,7 @@ func Test_AddPeer_PassesReadonlyWhenVersionsMatch(t *testing.T) { Peers: map[string]*Peer{}, version: NewVersionProviderTest(), eventDispatcher: event.DispatcherMock{}, + env: &clusterIdEnv{}, } p := &Peer{Version: testVersion("1")} @@ -69,6 +71,7 @@ func Test_AddPeer_TurnsReadonlyWhenVersionsDoNotMatch(t *testing.T) { Peers: map[string]*Peer{}, version: NewVersionProviderTest(), eventDispatcher: event.DispatcherMock{}, + env: &clusterIdEnv{}, } p := &Peer{Version: testVersion("dne")} @@ -125,6 +128,85 @@ func testVersion(v string) *versions.VersionInfo { return &versions.VersionInfo{Version: v} } +func Test_peersWithMismatchedClusterId(t *testing.T) { + t.Run("returns nothing when local cluster id is empty", func(t *testing.T) { + peers := map[string]*Peer{ + "a": {Id: "a", ClusterId: "cluster-1"}, + } + assert.Empty(t, peersWithMismatchedClusterId("", peers)) + }) + + t.Run("returns nothing when all peers match", func(t *testing.T) { + peers := map[string]*Peer{ + "a": {Id: "a", ClusterId: "cluster-1"}, + "b": {Id: "b", ClusterId: "cluster-1"}, + } + assert.Empty(t, peersWithMismatchedClusterId("cluster-1", peers)) + }) + + t.Run("ignores a peer with an empty cluster id", func(t *testing.T) { + // A blank peer is a legitimate joiner that has not yet adopted a cluster id. + peers := map[string]*Peer{ + "a": {Id: "a", ClusterId: ""}, + } + assert.Empty(t, peersWithMismatchedClusterId("cluster-1", peers)) + }) + + t.Run("returns only the peers whose cluster id differs", func(t *testing.T) { + mismatch := &Peer{Id: "mismatch", ClusterId: "cluster-2"} + peers := map[string]*Peer{ + "match": {Id: "match", ClusterId: "cluster-1"}, + "blank": {Id: "blank", ClusterId: ""}, + "mismatch": mismatch, + } + assert.Equal(t, []*Peer{mismatch}, peersWithMismatchedClusterId("cluster-1", peers)) + }) +} + +// closeRecordingChannel is a channel.Channel that records whether Close was called. Only Close is +// exercised by RevalidatePeerClusterIds; the embedded nil interface satisfies the rest. +type closeRecordingChannel struct { + channel.Channel + closed bool +} + +func (self *closeRecordingChannel) Close() error { + self.closed = true + return nil +} + +// clusterIdEnv is a mesh Env that reports a fixed cluster id. Only GetClusterId is exercised by +// RevalidatePeerClusterIds; the embedded nil interface satisfies the rest. +type clusterIdEnv struct { + Env + clusterId string +} + +func (self *clusterIdEnv) GetClusterId() string { + return self.clusterId +} + +func Test_RevalidatePeerClusterIds_ClosesOnlyMismatchedPeers(t *testing.T) { + matchCh := &closeRecordingChannel{} + blankCh := &closeRecordingChannel{} + mismatchCh := &closeRecordingChannel{} + + m := &impl{ + env: &clusterIdEnv{clusterId: "cluster-1"}, + Peers: map[string]*Peer{ + "match": {Id: "match", ClusterId: "cluster-1", Channel: matchCh}, + "blank": {Id: "blank", ClusterId: "", Channel: blankCh}, + "mismatch": {Id: "mismatch", ClusterId: "cluster-2", Channel: mismatchCh}, + }, + } + + m.RevalidatePeerClusterIds() + + assert.False(t, matchCh.closed, "peer with matching cluster id should not be closed") + assert.False(t, blankCh.closed, "peer with an empty cluster id should not be closed") + assert.True(t, mismatchCh.closed, "peer with a mismatched cluster id should be closed") +} + type VersionProviderTest struct { } diff --git a/controller/raft/mesh/signing_certs_test.go b/controller/raft/mesh/signing_certs_test.go new file mode 100644 index 000000000..e7f60b52b --- /dev/null +++ b/controller/raft/mesh/signing_certs_test.go @@ -0,0 +1,124 @@ +package mesh + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// newTestCertChain creates a self-signed root and a leaf issued by it, returning [leaf, root]. +func newTestCertChain(name string) ([]*x509.Certificate, error) { + rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + rootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: name + "-root"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + rootDer, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey) + if err != nil { + return nil, err + } + rootCert, err := x509.ParseCertificate(rootDer) + if err != nil { + return nil, err + } + + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: name + "-leaf"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + leafDer, err := x509.CreateCertificate(rand.Reader, leafTemplate, rootCert, &leafKey.PublicKey, rootKey) + if err != nil { + return nil, err + } + leafCert, err := x509.ParseCertificate(leafDer) + if err != nil { + return nil, err + } + + return []*x509.Certificate{leafCert, rootCert}, nil +} + +func Test_signingCertsFromHeaders(t *testing.T) { + chain, err := newTestCertChain("mesh-test") + require.NoError(t, err) + + t.Run("chain header yields the full chain", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + SigningCertChainHeader: ConcatDer([][]byte{chain[0].Raw, chain[1].Raw}), + SigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 2) + req.True(certs[0].Equal(chain[0])) + req.True(certs[1].Equal(chain[1])) + }) + + t.Run("falls back to the single-cert header when no chain header present", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + SigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 1) + req.True(certs[0].Equal(chain[0])) + }) + + t.Run("falls back to the legacy single-cert header", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + LegacySigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 1) + req.True(certs[0].Equal(chain[0])) + }) + + t.Run("unparsable chain header falls back to the single-cert header", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + SigningCertChainHeader: []byte("not a certificate"), + SigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 1) + req.True(certs[0].Equal(chain[0])) + }) + + t.Run("no cert headers yields nil", func(t *testing.T) { + req := require.New(t) + certs := signingCertsFromHeaders(map[int32][]byte{}) + req.Nil(certs) + }) +} diff --git a/controller/raft/raft.go b/controller/raft/raft.go index 0f7df0618..abfdb6cbf 100644 --- a/controller/raft/raft.go +++ b/controller/raft/raft.go @@ -41,7 +41,6 @@ import ( "github.com/openziti/foundation/v2/versions" "github.com/openziti/identity" "github.com/openziti/metrics" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/cmd_pb" "github.com/openziti/ziti/v2/common/pb/ctrl_pb" "github.com/openziti/ziti/v2/controller/apierror" @@ -53,6 +52,7 @@ import ( "github.com/openziti/ziti/v2/controller/model" "github.com/openziti/ziti/v2/controller/peermsg" "github.com/openziti/ziti/v2/controller/raft/mesh" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/sirupsen/logrus" "github.com/teris-io/shortid" ) @@ -138,6 +138,7 @@ func NewController(env Env, migrationMgr MigrationManager) *Controller { clusterEvents: make(chan raft.Observation, 16), raftRateLimiter: command.NewAdaptiveRateLimitTracker(env.GetRaftRateLimiterConfig(), env.GetMetricsRegistry(), env.GetCloseNotify()), errorMappers: map[string]func(map[string]any) error{}, + decoders: command.NewDecoders(), } result.initErrorMappers() return result @@ -157,10 +158,18 @@ type Controller struct { indexTracker IndexTracker migrationMgr MigrationManager clusterStateChangeHandlers concurrenz.CopyOnWriteSlice[func(event ClusterEvent, state ClusterState, leaderId string)] + clusterStateChangeLock sync.Mutex isLeader atomic.Bool clusterEvents chan raft.Observation raftRateLimiter rate.AdaptiveRateLimitTracker errorMappers map[string]func(map[string]any) error + decoders command.Decoders +} + +// GetDecoders returns the command decoder registry this controller's raft FSM uses to decode +// replicated log entries. It is per-controller so multiple in-process controllers stay isolated. +func (self *Controller) GetDecoders() command.Decoders { + return self.decoders } func (self *Controller) GetNodeId() *identity.TokenId { @@ -199,6 +208,9 @@ func (self *Controller) GetListenerHeaders() map[int32][]byte { if self.Config.PreferredLeader { headers[mesh.PreferredLeaderHeader] = []byte{1} } + if serverCerts := self.env.GetId().ServerCert(); len(serverCerts) > 0 && len(serverCerts[0].Certificate) > 0 { + headers[mesh.SigningCertChainHeader] = mesh.ConcatDer(serverCerts[0].Certificate) + } return headers } @@ -208,6 +220,10 @@ func (self *Controller) initErrorMappers() { } func (self *Controller) RegisterClusterEventHandler(f func(event ClusterEvent, state ClusterState, leaderId string)) { + // Hold the lock across the leader check and append so a leadership transition cannot slip + // between them, which would leave the handler seeing neither the immediate call nor the event. + self.clusterStateChangeLock.Lock() + defer self.clusterStateChangeLock.Unlock() if self.isLeader.Load() { f(ClusterEventLeadershipGained, newClusterState(true, !self.Mesh.IsReadOnly()), self.env.GetId().Token) } @@ -215,12 +231,8 @@ func (self *Controller) RegisterClusterEventHandler(f func(event ClusterEvent, s } func (self *Controller) InitEnv(env model.Env) error { + // The cluster id is loaded earlier, in Init, so it is set before raft and the mesh start. model.RegisterCommand(env, &InitClusterIdCmd{}, &cmd_pb.InitClusterIdCommand{}) - clusterId, err := db.LoadClusterId(env.GetDb()) - if err != nil { - return err - } - self.clusterId.Store(clusterId) return nil } @@ -634,12 +646,20 @@ func (self *Controller) Init() error { self.clusterEvents <- obs }) - self.Fsm = NewFsm(raftConfig.DataDir, raftConfig.RestartSelf, command.GetDefaultDecoders(), self.indexTracker, self.env.GetEventDispatcher()) + self.Fsm = NewFsm(raftConfig.DataDir, raftConfig.RestartSelf, self.decoders, self.indexTracker, self.env.GetEventDispatcher()) if err = self.Fsm.Init(); err != nil { return fmt.Errorf("failed to init FSM (%w)", err) } + // Load the cluster id before raft (and the mesh) start, so this node never presents an empty id + // that the mesh empty-id bypass would let pair with a different cluster. + clusterId, err := db.LoadClusterId(self.Fsm.GetDb()) + if err != nil { + return fmt.Errorf("failed to load cluster id (%w)", err) + } + self.clusterId.Store(clusterId) + raftTransport := raft.NewNetworkTransportWithLogger(self.Mesh, 3, 10*time.Second, raftConfig.Logger) if raftConfig.Recover { @@ -682,6 +702,7 @@ func (self *Controller) StartEventGeneration() { self.addEventsHandlers() go self.eventLoop() self.setupPreferredLeaderTransfer() + self.setupClusterIdBackfill() } func (self *Controller) setupPreferredLeaderTransfer() { @@ -762,6 +783,38 @@ func (self *Controller) transferToPreferredLeader() { log.Warn("no preferred leader peers are connected, retaining leadership") } +// setupClusterIdBackfill backfills a cluster id on leadership when the cluster has none, letting a +// cluster migrated on an older build (which came up with no cluster id) self-heal. If a cluster +// hasn't been bootstrapped yet, this isn't needed +func (self *Controller) setupClusterIdBackfill() { + if self.Raft.LastIndex() == 0 { + return + } + self.RegisterClusterEventHandler(func(evt ClusterEvent, state ClusterState, leaderId string) { + if evt == ClusterEventLeadershipGained { + go self.backfillClusterId() + } + }) +} + +// backfillClusterId sets a cluster id if this leader finds none; a no-op otherwise, so it is safe to +// run on every leadership change. The current timeline id is passed through unchanged. +func (self *Controller) backfillClusterId() { + if !self.IsLeader() || self.GetClusterId() != "" { + return + } + clusterId := uuid.NewString() + log := pfxlog.Logger().WithField("clusterId", clusterId) + log.Info("cluster has no cluster id; backfilling one on leadership acquisition") + if err := self.Dispatch(&InitClusterIdCmd{ + ClusterId: clusterId, + TimelineId: self.env.TimelineId(), + raftController: self, + }); err != nil { + log.WithError(err).Error("failed to backfill cluster id") + } +} + func (self *Controller) Configure(ctrlConfig *config.RaftConfig, conf *raft.Config) { conf.SnapshotThreshold = uint64(ctrlConfig.SnapshotThreshold) conf.SnapshotInterval = ctrlConfig.SnapshotInterval @@ -855,6 +908,9 @@ func (self *Controller) processRaftObservation(observation raft.Observation, eve pfxlog.Logger().Tracef("raft observation received: isLeader: %v, isReadWrite: %v", self.isLeader.Load(), eventState.isReadWrite) if raftState, ok := observation.Data.(raft.RaftState); ok { + // Serialize the leadership swap and dispatch with RegisterClusterEventHandler so a handler + // registered during a transition is not missed by both the immediate call and the event. + self.clusterStateChangeLock.Lock() if raftState == raft.Leader { if wasLeader := self.isLeader.Swap(true); !wasLeader { self.handleClusterStateChange(ClusterEventLeadershipGained, eventState) @@ -862,6 +918,7 @@ func (self *Controller) processRaftObservation(observation raft.Observation, eve } else if wasLeader := self.isLeader.Swap(false); wasLeader { self.handleClusterStateChange(ClusterEventLeadershipLost, eventState) } + self.clusterStateChangeLock.Unlock() } if state, ok := observation.Data.(mesh.ClusterState); ok { @@ -904,6 +961,17 @@ func (self *Controller) Bootstrap() error { logrus.Info("raft already bootstrapped") self.bootstrapped.Store(true) } else { + // Already connected to peers means this node belongs to an existing cluster; founding a new + // one here would fork a divergent cluster. + if peers := self.Mesh.GetPeers(); len(peers) > 0 { + addrs := make([]string, 0, len(peers)) + for addr := range peers { + addrs = append(addrs, addr) + } + return fmt.Errorf("refusing to bootstrap a new cluster: node is already connected to %d cluster peer(s) %v; "+ + "this node should join the existing cluster (e.g. 'ziti agent cluster add' from a current member), not initialize a new one", len(peers), addrs) + } + if err := self.migrationMgr.ValidateMigrationEnvironment(); err != nil { return err } @@ -1097,14 +1165,19 @@ type MigrationManager interface { InitializeRaftFromBoltDb(srcDb string) error } +var _ command.CriticalCommand = (*InitClusterIdCmd)(nil) + type InitClusterIdCmd struct { ClusterId string `json:"clusterId"` TimelineId string `json:"timelineId"` raftController *Controller } +// IsCriticalCommand marks InitClusterIdCmd as base state: it establishes the cluster id, which is +// not otherwise replayed, so a failed apply must halt rather than advance. Its writes are idempotent. +func (self *InitClusterIdCmd) IsCriticalCommand() {} + func (self *InitClusterIdCmd) Apply(ctx boltz.MutateContext) error { - self.raftController.clusterId.Store(self.ClusterId) _, err := self.raftController.Fsm.GetDb().GetTimelineId(boltz.TimelineModeForceReset, func() (string, error) { return self.TimelineId, nil }) @@ -1115,7 +1188,21 @@ func (self *InitClusterIdCmd) Apply(ctx boltz.MutateContext) error { if self.raftController.env.TimelineId() != self.TimelineId { self.raftController.env.InitTimelineId(self.TimelineId) } - return db.InitClusterId(self.raftController.Fsm.GetDb(), ctx, self.ClusterId) + + // Persist before publishing in memory. InitClusterId returns the effective id (an existing id + // wins), so a redundant command cannot diverge memory from disk. + effectiveClusterId, err := db.InitClusterId(self.raftController.Fsm.GetDb(), ctx, self.ClusterId) + if err != nil { + return err + } + + // Publish and revalidate only after persisting. Revalidation drops peers from a different + // cluster that connected while this node was blank; off the apply path so it does not stall raft. + self.raftController.clusterId.Store(effectiveClusterId) + if mesh := self.raftController.Mesh; mesh != nil { + go mesh.RevalidatePeerClusterIds() + } + return nil } func (self *InitClusterIdCmd) Encode() ([]byte, error) { diff --git a/controller/sync_strats/public_key_test.go b/controller/sync_strats/public_key_test.go new file mode 100644 index 000000000..f06d901a1 --- /dev/null +++ b/controller/sync_strats/public_key_test.go @@ -0,0 +1,50 @@ +package sync_strats + +import ( + "crypto/sha1" + "fmt" + "testing" + + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "github.com/stretchr/testify/require" +) + +func Test_newPublicKey(t *testing.T) { + data := []byte("anchor-cert-der") + intermediate1 := []byte("intermediate-1-der") + intermediate2 := []byte("intermediate-2-der") + + t.Run("sets kid from data fingerprint and carries usages and intermediates", func(t *testing.T) { + req := require.New(t) + + publicKey := newPublicKey(data, edge_ctrl_pb.DataState_PublicKey_X509CertDer, firstPartyCaUsages, intermediate1, intermediate2) + + req.Equal(data, publicKey.Data) + req.Equal(fmt.Sprintf("%x", sha1.Sum(data)), publicKey.Kid) + req.Equal(firstPartyCaUsages, publicKey.Usages) + req.Equal(edge_ctrl_pb.DataState_PublicKey_X509CertDer, publicKey.Format) + req.Equal([][]byte{intermediate1, intermediate2}, publicKey.Intermediates) + }) + + t.Run("no intermediates yields empty intermediates", func(t *testing.T) { + req := require.New(t) + + publicKey := newPublicKey(data, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages) + + req.Empty(publicKey.Intermediates) + }) + + t.Run("usage sets pair the deprecated usage with the party-specific usage", func(t *testing.T) { + req := require.New(t) + + req.Equal([]edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_JWTValidation}, controllerCertUsages) + + req.Contains(firstPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) + req.Contains(firstPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) + req.NotContains(firstPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation) + + req.Contains(thirdPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) + req.Contains(thirdPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation) + req.NotContains(thirdPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) + }) +} diff --git a/controller/sync_strats/rtx.go b/controller/sync_strats/rtx.go index 19560bd56..15f383025 100644 --- a/controller/sync_strats/rtx.go +++ b/controller/sync_strats/rtx.go @@ -335,8 +335,18 @@ type routerTxMap struct { internalMap cmap.ConcurrentMap[string, *RouterSender] //id -> RouterSender } +// Add installs routerMessageTxer as the sender for id, stopping any sender it replaces. Stopping the +// replaced sender here (rather than relying on RouterDisconnected) is required because the broker +// dispatches the old connection's RouterDisconnected asynchronously: on a reconnect/takeover the new +// connection's RouterConnected can install its sender before that async cleanup runs, which would +// otherwise orphan the old sender's goroutine. func (m *routerTxMap) Add(id string, routerMessageTxer *RouterSender) { - m.internalMap.Set(id, routerMessageTxer) + m.internalMap.Upsert(id, routerMessageTxer, func(exists bool, old *RouterSender, newValue *RouterSender) *RouterSender { + if exists && old != nil && old != newValue { + old.Stop() + } + return newValue + }) } func (m *routerTxMap) Get(id string) *RouterSender { diff --git a/controller/sync_strats/rtx_add_test.go b/controller/sync_strats/rtx_add_test.go new file mode 100644 index 000000000..f18dd164c --- /dev/null +++ b/controller/sync_strats/rtx_add_test.go @@ -0,0 +1,53 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package sync_strats + +import ( + "testing" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" +) + +// TestRouterTxMap_AddStopsReplaced verifies routerTxMap.Add stops the RouterSender it replaces. This is +// required under reject-if-busy: on a reconnect/takeover the new connection's RouterConnected can Add its +// sender before the old connection's RouterDisconnected runs (the broker dispatches it asynchronously), +// so without this the replaced sender's goroutine would be orphaned. +func TestRouterTxMap_AddStopsReplaced(t *testing.T) { + m := &routerTxMap{internalMap: cmap.New[*RouterSender]()} + + old := &RouterSender{closeNotify: make(chan struct{})} + old.running.Store(true) + m.Add("r1", old) + + newRtx := &RouterSender{closeNotify: make(chan struct{})} + newRtx.running.Store(true) + m.Add("r1", newRtx) + + require.False(t, old.running.Load(), "replaced sender should be stopped") + select { + case <-old.closeNotify: + default: + t.Fatal("replaced sender's closeNotify should be closed") + } + require.True(t, newRtx.running.Load(), "installed sender should still be running") + require.Equal(t, newRtx, m.Get("r1")) + + // Re-adding the same instance must not stop it. + m.Add("r1", newRtx) + require.True(t, newRtx.running.Load(), "re-adding the same sender must not stop it") +} diff --git a/controller/sync_strats/sync_instant.go b/controller/sync_strats/sync_instant.go index c6c1b0ff0..fd1b179fe 100644 --- a/controller/sync_strats/sync_instant.go +++ b/controller/sync_strats/sync_instant.go @@ -19,7 +19,7 @@ package sync_strats import ( "context" "crypto/sha1" - "crypto/tls" + "crypto/x509" "encoding/binary" "encoding/json" "fmt" @@ -123,18 +123,6 @@ func (strategy *InstantStrategy) NextIndex(ctx boltz.MutateContext) (uint64, err return strategy.indexProvider.NextIndex(ctx) } -func (strategy *InstantStrategy) AddPublicKey(cert *tls.Certificate) { - publicKey := newPublicKey(cert.Certificate[0], edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, edge_ctrl_pb.DataState_PublicKey_JWTValidation}) - newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: publicKey} - newEvent := &edge_ctrl_pb.DataState_Event{ - Action: edge_ctrl_pb.DataState_Create, - Model: newModel, - IsSynthetic: true, - } - - strategy.HandlePublicKeyEvent(newEvent, newModel) -} - // Initialize implements RouterDataModelCache func (strategy *InstantStrategy) Initialize(logSize uint64, bufferSize uint) error { strategy.RouterDataModelSender = common.NewRouterDataModelSender(strategy.ae, logSize, bufferSize) @@ -779,10 +767,12 @@ func (strategy *InstantStrategy) synchronize(rtx *RouterSender) { } for _, pk := range pks { + // Send the stored key as-is: rebuilding it field-by-field silently drops fields + // (this synthetic set bypasses router index checks and overwrites the full-sync copy). peerEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, Model: &edge_ctrl_pb.DataState_Event_PublicKey{ - PublicKey: newPublicKey(pk.Data, pk.Format, pk.Usages), + PublicKey: pk, }, IsSynthetic: true, } @@ -904,7 +894,11 @@ func (strategy *InstantStrategy) BuildServicePolicies(tx *bbolt.Tx, rdm *common. func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.RouterDataModelSender) error { serverTls := strategy.ae.HostController.Identity().ServerCert() - newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(serverTls[0].Certificate[0], edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_JWTValidation, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})} + // Controller certs are published leaf-only: routers use them solely for JWT validation, which + // needs no chain, and a controller cert's kid is emitted by several paths (identity TLS chain + // here, controller store records below and on create/update events). Identical content keeps + // the sender and router models convergent under last-writer-wins by kid. + newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(serverTls[0].Certificate[0], edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)} newEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, Model: newModel, @@ -923,7 +917,7 @@ func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.Route } certs := nfPem.PemStringToCertificates(storeModel.CertPem) - newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_JWTValidation, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})} + newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)} newEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, Model: newModel, @@ -935,9 +929,18 @@ func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.Route caPEMs := strategy.ae.GetConfig().Edge.CaPems() caCerts := nfPem.PemBytesToCertificates(caPEMs) + // Non-root CA certs in the bundle are intermediates; publish them on every root from + // the same bundle. Verification treats intermediates as a pool, so extras are harmless. + var caIntermediates [][]byte + for _, caCert := range caCerts { + if caCert.IsCA && !identity.IsRootCa(caCert) { + caIntermediates = append(caIntermediates, caCert.Raw) + } + } + for _, caCert := range caCerts { if identity.IsRootCa(caCert) { - publicKey := newPublicKey(caCert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation}) + publicKey := newPublicKey(caCert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, firstPartyCaUsages, caIntermediates...) newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: publicKey} newEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, @@ -965,7 +968,7 @@ func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.Route continue } - publicKey := newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation}) + publicKey := newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages, certsToRaw(certs[1:])...) newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: publicKey} newEvent := &edge_ctrl_pb.DataState_Event{ @@ -1605,13 +1608,44 @@ func newService(storeModel *db.EdgeService) *edge_ctrl_pb.DataState_Service { } } -func newPublicKey(data []byte, format edge_ctrl_pb.DataState_PublicKey_Format, usages []edge_ctrl_pb.DataState_PublicKey_Usage) *edge_ctrl_pb.DataState_PublicKey { - return &edge_ctrl_pb.DataState_PublicKey{ - Data: data, - Kid: fmt.Sprintf("%x", sha1.Sum(data)), - Usages: usages, - Format: format, +// Usage sets for published public keys. ClientX509CertValidation is deprecated but still +// emitted on CA anchors so routers predating the first/third-party usages keep validating +// client certs. Controller certs carry only JWTValidation: a controller identity is never a +// CA, so its certs anchor no client cert chains — trust anchors come from the CA bundles. +var ( + controllerCertUsages = []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_JWTValidation, } + firstPartyCaUsages = []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + } + thirdPartyCaUsages = []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation, + } +) + +// newPublicKey builds a DataState_PublicKey for data, deriving the kid from its SHA-1 +// fingerprint. Any intermediates are CA certs, in the same format, chaining data's issued +// certs to data. +func newPublicKey(data []byte, format edge_ctrl_pb.DataState_PublicKey_Format, usages []edge_ctrl_pb.DataState_PublicKey_Usage, intermediates ...[]byte) *edge_ctrl_pb.DataState_PublicKey { + return &edge_ctrl_pb.DataState_PublicKey{ + Data: data, + Kid: fmt.Sprintf("%x", sha1.Sum(data)), + Usages: usages, + Format: format, + Intermediates: intermediates, + } +} + +// certsToRaw returns the raw DER bytes of each certificate. +func certsToRaw(certs []*x509.Certificate) [][]byte { + var result [][]byte + for _, cert := range certs { + result = append(result, cert.Raw) + } + return result } func newPostureCheckById(tx *bbolt.Tx, ae *env.AppEnv, id string) (*edge_ctrl_pb.DataState_PostureCheck, error) { @@ -1847,21 +1881,19 @@ func (strategy *InstantStrategy) PostureCheckDelete(index uint64, postureCheck * func (strategy *InstantStrategy) ControllerCreate(index uint64, controller *db.Controller) { certs := nfPem.PemStringToCertificates(controller.CertPem) - cert := certs[0] - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(cert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, edge_ctrl_pb.DataState_PublicKey_JWTValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)) } func (strategy *InstantStrategy) ControllerUpdate(index uint64, controller *db.Controller) { certs := nfPem.PemStringToCertificates(controller.CertPem) - cert := certs[0] - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(cert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, edge_ctrl_pb.DataState_PublicKey_JWTValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)) } func (strategy *InstantStrategy) CaCreate(index uint64, ca *db.Ca) { certs := nfPem.PemBytesToCertificates([]byte(ca.CertPem)) if len(certs) > 0 { - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages, certsToRaw(certs[1:])...)) } } @@ -1869,7 +1901,7 @@ func (strategy *InstantStrategy) CaUpdate(index uint64, ca *db.Ca) { certs := nfPem.PemBytesToCertificates([]byte(ca.CertPem)) if len(certs) > 0 { - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Update, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Update, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages, certsToRaw(certs[1:])...)) } } @@ -1877,7 +1909,7 @@ func (strategy *InstantStrategy) CaDelete(index uint64, ca *db.Ca) { certs := nfPem.PemBytesToCertificates([]byte(ca.CertPem)) if len(certs) > 0 { - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Delete, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Delete, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages)) } } diff --git a/controller/webapis/client-api.go b/controller/webapis/client-api.go index 9e8c1501a..0087de72e 100644 --- a/controller/webapis/client-api.go +++ b/controller/webapis/client-api.go @@ -26,7 +26,6 @@ import ( "github.com/openziti/edge-api/rest_client_api_client" "github.com/openziti/edge-api/rest_client_api_server" "github.com/openziti/edge-api/rest_management_api_server" - "github.com/openziti/foundation/v2/errorz" "github.com/openziti/xweb/v3" "github.com/openziti/ziti/v2/controller/api" "github.com/openziti/ziti/v2/controller/apierror" @@ -195,7 +194,8 @@ func (clientApi ClientApiHandler) newHandler(ae *env.AppEnv) http.Handler { rc, err := ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) + return } api.AddRequestContextToHttpContext(r, rc) diff --git a/controller/webapis/fabric-management-api.go b/controller/webapis/fabric-management-api.go index 2040aabb2..9310bd05a 100644 --- a/controller/webapis/fabric-management-api.go +++ b/controller/webapis/fabric-management-api.go @@ -187,7 +187,7 @@ func (self *FabricManagementApiHandler) WrapHttpHandler(handler http.Handler) ht rc, err := self.ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) return } @@ -208,7 +208,7 @@ func (self *FabricManagementApiHandler) WrapWsHandler(handler http.Handler) http rc, err := self.ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) return } diff --git a/controller/webapis/management-api.go b/controller/webapis/management-api.go index 6f550c1aa..96772c3c8 100644 --- a/controller/webapis/management-api.go +++ b/controller/webapis/management-api.go @@ -24,7 +24,6 @@ import ( "github.com/openziti/edge-api/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_server" - "github.com/openziti/foundation/v2/errorz" "github.com/openziti/xweb/v3" "github.com/openziti/ziti/v2/controller/api" "github.com/openziti/ziti/v2/controller/apierror" @@ -138,7 +137,7 @@ func (managementApi ManagementApiHandler) newHandler(ae *env.AppEnv) http.Handle rc, err := ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) return } diff --git a/controller/webapis/metrics-api.go b/controller/webapis/metrics-api.go index 313a3d39a..bd26cecaf 100644 --- a/controller/webapis/metrics-api.go +++ b/controller/webapis/metrics-api.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" @@ -147,8 +148,15 @@ func (metricsApi *MetricsApiHandler) newHandler() http.Handler { if nil != metricsApi.scrapeCert { certOk := false - for _, r := range r.TLS.PeerCertificates { - if bytes.Equal(metricsApi.scrapeCert.Signature, r.Signature) { + // Match the pinned scrape cert against the presented LEAF only (PeerCertificates[0], the + // cert whose private key the TLS handshake proved) - iterating the whole chain would let a + // client present its own leaf plus the public scrape cert as an extra cert and pass. Compare + // full DER, not just the signature, and reject a leaf outside its validity window. + if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { + leaf := r.TLS.PeerCertificates[0] + now := time.Now() + if bytes.Equal(metricsApi.scrapeCert.Raw, leaf.Raw) && + !now.Before(leaf.NotBefore) && !now.After(leaf.NotAfter) { certOk = true } } diff --git a/controller/webapis/metrics-api_test.go b/controller/webapis/metrics-api_test.go new file mode 100644 index 000000000..165e38799 --- /dev/null +++ b/controller/webapis/metrics-api_test.go @@ -0,0 +1,103 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package webapis + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var mSerial int64 + +func mMkCert(t *testing.T, cn string, notBefore, notAfter time.Time) *x509.Certificate { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + mSerial++ + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(mSerial), + Subject: pkix.Name{CommonName: cn}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return c +} + +func mScrapeRequest(peerCerts []*x509.Certificate) *http.Request { + r := httptest.NewRequest(http.MethodGet, "/metrics", nil) + if peerCerts != nil { + r.TLS = &tls.ConnectionState{PeerCertificates: peerCerts} + } + return r +} + +// Test_MetricsApi_ScrapeCertAuthorization covers the scrape-cert gate on the metrics endpoint. When a +// scrape cert is pinned, authorization must match the pinned cert against the presented LEAF only +// (PeerCertificates[0]), compare the full certificate, honor the validity window, and reject requests +// with no client certificate. Only the rejecting (401) paths are exercised here; they return before the +// handler consults the network. +func Test_MetricsApi_ScrapeCertAuthorization(t *testing.T) { + now := time.Now() + scrapeCert := mMkCert(t, "scrape", now.Add(-time.Hour), now.Add(time.Hour)) + + handler := (&MetricsApiHandler{scrapeCert: scrapeCert}).newHandler() + + assertUnauthorized := func(t *testing.T, peerCerts []*x509.Certificate) { + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, mScrapeRequest(peerCerts)) + require.Equal(t, http.StatusUnauthorized, rec.Code) + } + + t.Run("no client certificate", func(t *testing.T) { + assertUnauthorized(t, nil) + }) + + t.Run("presented leaf is not the scrape cert", func(t *testing.T) { + other := mMkCert(t, "other", now.Add(-time.Hour), now.Add(time.Hour)) + assertUnauthorized(t, []*x509.Certificate{other}) + }) + + t.Run("scrape cert presented only as a filler cert, not the leaf", func(t *testing.T) { + attacker := mMkCert(t, "attacker", now.Add(-time.Hour), now.Add(time.Hour)) + // The pinned scrape cert is present in the chain, but the leaf (index 0) is the attacker's. + assertUnauthorized(t, []*x509.Certificate{attacker, scrapeCert}) + }) + + t.Run("expired scrape cert presented as the leaf", func(t *testing.T) { + expired := mMkCert(t, "expired", now.Add(-2*time.Hour), now.Add(-time.Hour)) + expiredHandler := (&MetricsApiHandler{scrapeCert: expired}).newHandler() + rec := httptest.NewRecorder() + expiredHandler.ServeHTTP(rec, mScrapeRequest([]*x509.Certificate{expired})) + require.Equal(t, http.StatusUnauthorized, rec.Code, "a leaf outside its validity window must be rejected") + }) +} diff --git a/etc/ctrl.with.edge.yml b/etc/ctrl.with.edge.yml index 9f0d03f6f..bf1d32fff 100644 --- a/etc/ctrl.with.edge.yml +++ b/etc/ctrl.with.edge.yml @@ -262,6 +262,141 @@ edge: # database. Only runs on the raft leader. # revocationEnforcerFrequency: 1m + # externalJwtSigners - optional + # Settings that govern how the controller interacts with external JWT signers. + externalJwtSigners: + # jwksFetch - optional + # Controls the controller's fetch of an external JWT signer's jwksEndpoint. That fetch is + # made by the controller, from the controller's own network position, to a URL supplied by + # whoever can write an external JWT signer. These settings constrain where it may go. + # + # A fetch is allowed only if it passes TWO GATES, and both gates are applied to the first + # request AND to every redirect that is followed: + # + # the HOSTNAME gate - checked against the hostname in the URL + # the ADDRESS gate - checked against the resolved address at connection time, so a + # hostname that resolves to a blocked address is still blocked + # + # NEITHER GATE CAN AUTHORIZE WHAT THE OTHER REFUSES. Hostname matching only ever narrows what + # may be fetched: allowedHostnames does not make a blocked address reachable, and + # allowedIPs does not make a blocked hostname reachable. + # + # HOSTNAME gate, first-match-wins. DENY WINS OVER ALLOW: + # + # 1. deniedHostnames - blocked + # 2. allowedHostnames - when set, ONLY these hostnames may be fetched; anything else is + # blocked + # 3. anything else - passes to the address gate + # + # ADDRESS gate, first-match-wins. DENY WINS OVER ALLOW: + # + # 1. built-in blocked addresses - always blocked, allowedIPs CANNOT override: + # cloud instance metadata (169.254.169.254, 169.254.170.2, fd00:ec2::254) + # link-local (169.254.0.0/16, fe80::/10) + # link-local multicast (224.0.0.0/24, ff02::/16) + # unspecified (0.0.0.0, ::) + # 2. deniedIPs - blocked, allowedIPs CANNOT override + # 3. allowedIPs - allowed; this is a carve-out of tier 4 ONLY + # 4. blockPrivateAddresses, and the address is private or loopback - blocked + # 5. anything else - allowed + # + # ENTRY FORMS. The two kinds of list do not accept each other's values. + # + # deniedIPs and allowedIPs take IP addresses only, in either of two forms: + # + # a flat IP address - 192.168.5.5 or fd00:1234::1 + # matches that one address only (the same as /32 or /128) + # a CIDR block - 10.10.0.0/16 or fd00:1234::/64 + # matches every address in the block + # + # Both IPv4 and IPv6 are accepted in either form. A hostname in one of these lists is a + # configuration error and the controller will not start: the address gate runs against the + # address actually being connected to, so there is no hostname there to compare against. + # + # deniedHostnames and allowedHostnames take hostnames only, in either of two forms: + # + # an exact hostname - idp.example.com + # matches that hostname only + # a wildcard suffix - '*.example.com' + # matches any subdomain of example.com, AT ANY DEPTH, but NEVER + # example.com itself + # + # Wildcard matching, spelled out, because the last part surprises people: + # + # '*.sub.host.com' MATCHES idp.sub.host.com + # MATCHES a.b.sub.host.com (any depth below the suffix) + # DOES NOT sub.host.com (the suffix itself is NOT matched) + # DOES NOT other.host.com + # DOES NOT xsub.host.com (a whole label must be replaced) + # + # To cover both a domain and its subdomains, list them both: + # + # - sub.host.com + # - '*.sub.host.com' + # + # The '*' is only supported as the entire leading label, written exactly as '*.'. Anything + # else (idp.*.example.com, *idp.example.com, a bare *) is a configuration error. Quote any + # entry that starts with '*', or YAML will reject it as an alias. Matching ignores case and + # a trailing dot, and a unicode hostname is compared in its punycode form. An IP address in + # one of these lists is a configuration error - use deniedIPs or allowedIPs. + jwksFetch: + # deniedHostnames - optional, default empty + # Tier 1 of the HOSTNAME gate: hostnames that may not be fetched. Takes exact hostnames + # and '*.suffix' wildcards, per ENTRY FORMS above. + # This narrows only, it is not a boundary on its own: the same target can be reached + # under a different name or as a literal IP, which is what the address gate is for. + # deniedHostnames: + # - old-idp.example.com + # - '*.internal.example.com' + + # allowedHostnames - optional, default empty + # Tier 2 of the HOSTNAME gate: when this list is non-empty, these are the ONLY hostnames that + # may be fetched, on the first request and on every redirect hop. Same entry forms as + # deniedHostnames, and deniedHostnames still wins. It cannot widen the address gate: a listed + # hostname whose address is blocked stays blocked. Note that while this list is set, an + # endpoint written as a literal IP address can never satisfy it. + # allowedHostnames: + # - idp.example.com + # - '*.idp.example.org' + + # blockPrivateAddresses - optional, default false + # Tier 4 of the ADDRESS gate: blocks private and loopback addresses + # (127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7). Defaults to + # false so a deployment whose IdP is on an internal address keeps working. Tier 1 is + # blocked either way, so the metadata service is never reachable. For the strictest + # posture set this to true, list any internal IdP address in allowedIPs, and list + # the IdP hostnames in allowedHostnames. + # blockPrivateAddresses: false + + # deniedIPs - optional, default empty + # Tier 2 of the ADDRESS gate: addresses that are always blocked. Takes flat IP addresses + # and CIDR blocks, per ENTRY FORMS above. Nothing in allowedIPs or allowedHostnames can + # re-enable an address listed here, so this is the right place for internal ranges that + # must never be fetched. + # deniedIPs: + # - 10.10.0.0/16 # a CIDR block + # - 192.168.5.5 # a flat IP address + # - fd00:1234::/64 # IPv6 is accepted in either form + + # allowedIPs - optional, default empty + # Tier 3 of the ADDRESS gate: a carve-out of tier 4 ONLY. Same entry forms as deniedIPs. + # Listing an address here allows it despite blockPrivateAddresses; it does NOT override + # tier 1 or deniedIPs, and it does NOT satisfy the hostname gate. Use it to reach a + # specific internal IdP while blockPrivateAddresses is true. + # allowedIPs: + # - 10.20.30.0/24 + # - 10.20.40.7 + + # timeout - optional, default 5s + # The total time allowed for a single JWKS fetch, including any redirects. Must be + # greater than zero. + # timeout: 5s + + # maxRedirects - optional, default 5 + # How many redirects a JWKS fetch will follow. Every hop passes through both gates. Set + # to 0 to refuse to follow redirects at all. + # maxRedirects: 5 + # Set to true to disable posture check functionality disablePostureChecks: false diff --git a/go.mod b/go.mod index 41e9df7b5..8e84bae01 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ replace github.com/michaelquigley/pfxlog => github.com/michaelquigley/pfxlog v0. require ( github.com/AppsFlyer/go-sundheit v0.6.0 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 github.com/Jeffail/gabs v1.4.0 github.com/Jeffail/gabs/v2 v2.7.0 @@ -24,18 +24,18 @@ require ( github.com/ef-ds/deque v1.0.4 github.com/fatih/color v1.19.0 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa - github.com/gaissmai/extnetip v1.3.1 + github.com/gaissmai/extnetip v1.3.2 github.com/go-acme/lego/v4 v4.35.2 github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-openapi/errors v0.22.8 - github.com/go-openapi/jsonpointer v0.24.0 - github.com/go-openapi/loads v0.24.0 - github.com/go-openapi/runtime v0.32.4 - github.com/go-openapi/spec v0.22.6 - github.com/go-openapi/strfmt v0.26.4 - github.com/go-openapi/swag v0.27.0 - github.com/go-openapi/swag/jsonutils v0.27.0 - github.com/go-openapi/validate v0.26.0 + github.com/go-openapi/jsonpointer v1.0.0 + github.com/go-openapi/loads v0.25.1 + github.com/go-openapi/runtime v0.33.0 + github.com/go-openapi/spec v0.22.9 + github.com/go-openapi/strfmt v0.27.0 + github.com/go-openapi/swag v0.29.0 + github.com/go-openapi/swag/jsonutils v0.29.0 + github.com/go-openapi/validate v0.26.3 github.com/go-resty/resty/v2 v2.17.2 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -50,7 +50,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/raft v1.7.3 github.com/hashicorp/raft-boltdb/v2 v2.3.1 - github.com/jedib0t/go-pretty/v6 v6.8.1 + github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/jellydator/ttlcache/v3 v3.4.1 github.com/jessevdk/go-flags v1.6.1 github.com/jinzhu/copier v0.4.0 @@ -59,13 +59,13 @@ require ( github.com/lucsky/cuid v1.2.1 github.com/mdlayher/netlink v1.11.2 github.com/michaelquigley/pfxlog v1.0.0 - github.com/miekg/dns v1.1.72 + github.com/miekg/dns v1.1.73 github.com/mitchellh/mapstructure v1.5.0 github.com/natefinch/lumberjack v2.0.0+incompatible github.com/openziti/agent v1.0.33 - github.com/openziti/channel/v4 v4.3.11 + github.com/openziti/channel/v4 v4.3.12 github.com/openziti/cobra-to-md v1.0.1 - github.com/openziti/edge-api v0.31.0 + github.com/openziti/edge-api v0.36.0 github.com/openziti/foundation/v2 v2.0.91 github.com/openziti/identity v1.0.129 github.com/openziti/jwks v1.0.6 @@ -78,31 +78,31 @@ require ( github.com/openziti/xweb/v3 v3.0.4 github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 - github.com/rabbitmq/amqp091-go v1.12.0 + github.com/rabbitmq/amqp091-go v1.14.0 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/rodaine/table v1.3.1 github.com/russross/blackfriday v1.6.0 github.com/shirou/gopsutil/v3 v3.24.5 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 github.com/xeipuuv/gojsonschema v1.2.0 - github.com/zitadel/oidc/v3 v3.47.5 + github.com/zitadel/oidc/v3 v3.49.2 go.etcd.io/bbolt v1.5.0 go.uber.org/atomic v1.11.0 go4.org v0.0.0-20260112195520-a5071408f32f - golang.org/x/crypto v0.53.0 - golang.org/x/net v0.56.0 + golang.org/x/crypto v0.55.0 + golang.org/x/net v0.58.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.21.0 - golang.org/x/sys v0.46.0 - golang.org/x/term v0.44.0 - golang.org/x/text v0.38.0 - google.golang.org/protobuf v1.36.11 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/term v0.45.0 + golang.org/x/text v0.41.0 + google.golang.org/protobuf v1.36.12 gopkg.in/AlecAivazis/survey.v1 v1.8.8 gopkg.in/resty.v1 v1.12.0 gopkg.in/yaml.v2 v2.4.0 @@ -112,11 +112,11 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect - github.com/Azure/go-amqp v1.6.0 // indirect + github.com/Azure/go-amqp v1.7.0 // indirect github.com/MichaelMure/go-term-text v0.3.1 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/andybalholm/brotli v1.2.1 // indirect - github.com/antchfx/xpath v1.3.6 // indirect + github.com/antchfx/xpath v1.3.8 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/boltdb/bolt v1.3.1 // indirect @@ -125,33 +125,32 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/creack/pty v1.1.11 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/go-chi/chi/v5 v5.2.5 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-chi/chi/v5 v5.3.1 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.25.2 // indirect - github.com/go-openapi/jsonreference v0.21.6 // indirect + github.com/go-openapi/analysis v0.26.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect - github.com/go-openapi/swag/cmdutils v0.27.0 // indirect - github.com/go-openapi/swag/conv v0.27.0 // indirect - github.com/go-openapi/swag/fileutils v0.27.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.1 // indirect - github.com/go-openapi/swag/loading v0.27.0 // indirect - github.com/go-openapi/swag/mangling v0.27.0 // indirect - github.com/go-openapi/swag/netutils v0.27.0 // indirect - github.com/go-openapi/swag/stringutils v0.27.0 // indirect - github.com/go-openapi/swag/typeutils v0.27.0 // indirect - github.com/go-openapi/swag/yamlutils v0.27.0 // indirect + github.com/go-openapi/swag/cmdutils v0.29.0 // indirect + github.com/go-openapi/swag/conv v0.29.0 // indirect + github.com/go-openapi/swag/fileutils v0.29.0 // indirect + github.com/go-openapi/swag/loading v0.29.0 // indirect + github.com/go-openapi/swag/mangling v0.29.0 // indirect + github.com/go-openapi/swag/netutils v0.29.0 // indirect + github.com/go-openapi/swag/pools v0.29.0 // indirect + github.com/go-openapi/swag/stringutils v0.29.0 // indirect + github.com/go-openapi/swag/typeutils v0.29.0 // indirect + github.com/go-openapi/swag/yamlutils v0.29.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-metrics v0.6.1 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -159,25 +158,24 @@ require ( github.com/kr/pty v1.1.8 // indirect github.com/kyokomi/emoji/v2 v2.2.13 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-runewidth v0.0.28 // indirect github.com/mattn/go-tty v0.0.8 // indirect - github.com/mdlayher/socket v0.6.0 // indirect + github.com/mdlayher/socket v0.6.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/pkcs11 v1.1.2 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/openziti/go-term-markdown v1.0.1 // indirect github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rs/cors v1.11.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -194,17 +192,14 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zitadel/logging v0.7.0 // indirect github.com/zitadel/schema v1.3.2 // indirect go.mozilla.org/pkcs7 v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/tools v0.46.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect nhooyr.io/websocket v1.8.17 // indirect ) diff --git a/go.sum b/go.sum index 2ab3db781..e9cd49828 100644 --- a/go.sum +++ b/go.sum @@ -41,16 +41,16 @@ github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliG github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk= github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 h1:kE5kpeiSqu4jcCQ/sWuyggMXJ/pT6oQ99+8hwPmyeJ0= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0/go.mod h1:IAN3Z0DMtehoxoQQnfqg1891z1P7GNoDryKtFcAyMBI= -github.com/Azure/go-amqp v1.6.0 h1:pMnBstxSd2JnvTopR/L9MUdQi4e5Mp9FscP4kZ0rZ8M= -github.com/Azure/go-amqp v1.6.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-amqp v1.7.0 h1:9VlH/LEWr386XWWJRNON0eslFqSClYBXP4HewvIqkDQ= +github.com/Azure/go-amqp v1.7.0/go.mod h1:pCJaHsvRlmmFUpxyQbh2qPkUFqYJeRBTqJSHKJadvPg= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -74,13 +74,13 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antchfx/jsonquery v1.3.7 h1:LUoue12xcCj6Q41kYUSAS0UJ+9s3XyxbP5uh7x8aMsw= github.com/antchfx/jsonquery v1.3.7/go.mod h1:oGh95SRUXZfnma1B7Q0p1rhgDeSgghub4W+JwnUYv2o= -github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antchfx/xpath v1.3.8 h1:RQlkLaJDKk1Ew1H6CUPUTKM+IQxm+6HTyOgcrfqOU9c= +github.com/antchfx/xpath v1.3.8/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -138,8 +138,6 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 h1:fctNkSsavbXpt8geFWZb8n+noCqS8MrOXRJ/YfdZ2dQ= @@ -164,8 +162,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -176,13 +174,13 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gaissmai/extnetip v1.3.1 h1:e8xa3hJG/+6NevjZXVkC60WBLypNjH/ugEy4kaPoKAU= -github.com/gaissmai/extnetip v1.3.1/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= +github.com/gaissmai/extnetip v1.3.2 h1:lNjdWx0pT6hXSfp4lc6jG1WPne0/PS2T8kRPYw26NsM= +github.com/gaissmai/extnetip v1.3.2/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU= github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -190,68 +188,66 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= -github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= +github.com/go-openapi/analysis v0.26.0 h1:1xECln1iMMmQnTjgcknC1vi1hA4KISt6IHpSwnqcuwI= +github.com/go-openapi/analysis v0.26.0/go.mod h1:40gERFi/2dyXA1FaqRRLxkv1IlC6X+GPDNd1xrYAjZE= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= -github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= -github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= -github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= -github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4= -github.com/go-openapi/runtime v0.32.4 h1:8ElGj/3goG0itt0nBPP6Cm57ehcYyuHoI3O20nxgvkw= -github.com/go-openapi/runtime v0.32.4/go.mod h1:Bz6keOZw1NX4T6f+m42OoT1MBPDt6Re13dbccHyGH/4= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/loads v0.25.1 h1:toKQdIDLxlqfKLLGUUmUsiTd5/X0Chzvde9EGYQP/Ac= +github.com/go-openapi/loads v0.25.1/go.mod h1:33Hen4tsKXHL45TyYojvfD5fZUFN4O1y4r/XhsRW2zc= +github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU= +github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw= github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= -github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= -github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= -github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= -github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= -github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE= -github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0= -github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= -github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= -github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= -github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8= -github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= -github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= -github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= -github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc= -github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= -github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= -github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= -github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= -github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= -github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= +github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= +github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= +github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= +github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= +github.com/go-openapi/swag v0.29.0 h1:vJ8m3Xv7L4VfGgr8nK77CHTQRg2nyFPXySpV0w3xPB8= +github.com/go-openapi/swag v0.29.0/go.mod h1:8FrS8OFgntDRBzpHD7SyqDJTmTDPZo8Kvv0OuQW+Mr4= +github.com/go-openapi/swag/cmdutils v0.29.0 h1:AKt8Q7ZfR2NmnSJD7B1WcnTOfuXHBvLPJc3W65SCM3Y= +github.com/go-openapi/swag/cmdutils v0.29.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.29.0 h1:4+1TogWpOIzMPzVKrvx1BfqBYlApB7D7DW3EAWpwmp4= +github.com/go-openapi/swag/conv v0.29.0/go.mod h1:ch1l7V87F6zQXuLs5s0RFvrro6aFvrVcfVXn2PTZnu8= +github.com/go-openapi/swag/fileutils v0.29.0 h1:meobnn3MsAkF6XmJn6qw3hPeMAOhwf7XD5BOKeFzqWU= +github.com/go-openapi/swag/fileutils v0.29.0/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI= +github.com/go-openapi/swag/jsonutils v0.29.0 h1:Xgnf9g32ycQjQUnDxkhqLraH2FhitcSE3w7ayQB3TgA= +github.com/go-openapi/swag/jsonutils v0.29.0/go.mod h1:5WYmjf6hJcBve+ArzBaUsYy4M1GXsgjIQTmwJKfZHrA= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0 h1:bpSF6LFkJJVtaRtJCzbZADVPVHQYKPwPdKthOQA2/5o= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg= +github.com/go-openapi/swag/loading v0.29.0 h1:r1lg2DQbT1VgBwgiPYXBM059RNswFI6r36CC0QCcRGw= +github.com/go-openapi/swag/loading v0.29.0/go.mod h1:l/Z4MNbom0jSqzvWJqK2VUUWEceBknGEuVbLHLq4KN0= +github.com/go-openapi/swag/mangling v0.29.0 h1:RVKyucZ2rvA/M/sqxuNZGW8Mf0+1qypVX8n2GwV11lM= +github.com/go-openapi/swag/mangling v0.29.0/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s= +github.com/go-openapi/swag/netutils v0.29.0 h1:2Y9tiqzdzRf++Tf5SGVK4Rk1iop2mo1+W3fNdcflMCo= +github.com/go-openapi/swag/netutils v0.29.0/go.mod h1:DUde7x4Bx00k5jYl2AdRpNAO0m7atUvD2x6X+bWkbno= +github.com/go-openapi/swag/pools v0.29.0 h1:uMQcoJeHJ8fWkdfEXJZMMpqk6hpfW8qTL5Q/IoRFFII= +github.com/go-openapi/swag/pools v0.29.0/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= +github.com/go-openapi/swag/stringutils v0.29.0 h1:/IEOuZ7PGJi6lqgH83dVt7/A9eHsDGEH1459lm+gpEo= +github.com/go-openapi/swag/stringutils v0.29.0/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= +github.com/go-openapi/swag/typeutils v0.29.0 h1:HrWCYZeXVVNDo/7QQPRaYk33XeIDxksbxpalID3bWR8= +github.com/go-openapi/swag/typeutils v0.29.0/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= +github.com/go-openapi/swag/yamlutils v0.29.0 h1:JOKKuhMnBx4HYTM+kPEYw8S5YKKU9PnC4Mwb+c69BBA= +github.com/go-openapi/swag/yamlutils v0.29.0/go.mod h1:/+FVozjFWZzku6mRz5U/Qmq5Yk8PLFxBLLWA/jHaxYE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= +github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= +github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.3 h1:OkfZgLvLDnGP2hrRGD+42WBiPWWkoHomTJ+IVI+KaDc= +github.com/go-openapi/validate v0.26.3/go.mod h1:7DOOa4raU6NRe7A8VQSKbm3VcuUIioREYHFt+er9Sk8= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -358,8 +354,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= -github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-metrics v0.6.1 h1:V7j9cgHTXl4OebnX3y0l6ZSoO5dTp0VI0K8Y7JfRsS4= +github.com/hashicorp/go-metrics v0.6.1/go.mod h1:XOozbQKeJz12GG8cCcQb/E5vNVeTdJt0aHbM+uHnL1M= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -400,8 +396,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= -github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= @@ -412,10 +408,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -423,7 +417,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/judedaryl/go-arrayutils v0.0.1 h1:89rWXRVp1c1gcE1UEWvFuohVMeYwfA0y4TMZtE8dS58= github.com/judedaryl/go-arrayutils v0.0.1/go.mod h1:vqtnlEkOBpDGHS3U3kQtMJZGTOC+SBFAQYj2KcxLf1A= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -431,7 +424,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -461,8 +453,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -470,29 +462,29 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= +github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.8 h1:yxtc0Ye17/1ne/bjy993YUoyP8bJJFa9n5M9XTdwoZQ= github.com/mattn/go-tty v0.0.8/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= -github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= -github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/michaelquigley/pfxlog v0.6.10 h1:IbC/H3MmSDcPlQHF1UZPQU13Dkrs0+ycWRyQd2ihnjw= github.com/michaelquigley/pfxlog v0.6.10/go.mod h1:gEiNTfKEX6cJHSwRpOuqBpc8oYrlhMiDK/xMk/gV7D0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -516,15 +508,14 @@ github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= @@ -537,12 +528,14 @@ github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/openziti/agent v1.0.33 h1:W6LXs+dWIzg/V8KNJq501zUxFSjtDQTaBFdqOcUFCTg= github.com/openziti/agent v1.0.33/go.mod h1:pjQ9jSOl+9ZR/0Y9+wOwJhGekYwyai2uKB3YCd0nulI= -github.com/openziti/channel/v4 v4.3.11 h1:ugezUhTEuSQnVFKUioN1gh6f+ZF4eBqx6mOrT5DrY5U= -github.com/openziti/channel/v4 v4.3.11/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= +github.com/openziti/channel/v4 v4.3.12 h1:kpox7z7aILokmzejUlOobORzQutH6q3t4E5abuZcOPA= +github.com/openziti/channel/v4 v4.3.12/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQfVXxE= github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c= github.com/openziti/edge-api v0.31.0 h1:QdaaPnKQj4B40q404+c8VZxMsoVpfGpfOhzs3LLCd/A= github.com/openziti/edge-api v0.31.0/go.mod h1:pDtCR6Mq0h5e8ulJhmuhPshEBa39hpQy+yTtLpUSBOs= +github.com/openziti/edge-api v0.36.0 h1:adp0gCDbxee3r4tPBCXm7IyLTcIGhDcWmQ9QJMO+AwY= +github.com/openziti/edge-api v0.36.0/go.mod h1:m1oAQ6+fnkEO0NOAkDy6WpnXDxPhj2sko4eBUvIMxkE= github.com/openziti/foundation/v2 v2.0.91 h1:gjXFu+fxKKMCs1hCkfLNI9jfdiSBW2gXAnBCy5xG/UI= github.com/openziti/foundation/v2 v2.0.91/go.mod h1:vbAudlD59QTMR+IifKXLL9c8W/OyL8DVsbYqGXWV2dY= github.com/openziti/go-term-markdown v1.0.1 h1:9uzMpK4tav6OtvRxRt99WwPTzAzCh+Pj9zWU2FBp3Qg= @@ -574,8 +567,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -592,31 +585,23 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rabbitmq/amqp091-go v1.12.0 h1:V0v14Iqfs+MwHWihJt/nGS5Ulu0vw572b2Co3mwunkI= -github.com/rabbitmq/amqp091-go v1.12.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rabbitmq/amqp091-go v1.14.0 h1:RSaT7aOKt/OrkVUyswPDW29lnRz9psuGmfZFBmLqLek= +github.com/rabbitmq/amqp091-go v1.14.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -646,10 +631,9 @@ github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsB github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= +github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -691,8 +675,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -724,10 +708,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= -github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= -github.com/zitadel/oidc/v3 v3.47.5 h1:cR2z0oqa5XZkwpXQiPCUGqKtndrjHgEXb81y3oXocK4= -github.com/zitadel/oidc/v3 v3.47.5/go.mod h1:XxFh0666HRXycyrKmono+3gY0RACpYJLgy4r/+kliKY= +github.com/zitadel/oidc/v3 v3.49.2 h1:yKvB2Hx6rVWp0vOTk1pEyXAPxJusgIHNmr20AxOohmQ= +github.com/zitadel/oidc/v3 v3.49.2/go.mod h1:HwoguOGo0eem0RK5Gb+P6Q4aQLVinJ9LhomlVEA57ck= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= @@ -761,8 +743,9 @@ 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.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -776,8 +759,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -788,8 +771,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -816,8 +799,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -862,8 +843,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -890,8 +871,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -918,7 +899,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -932,8 +912,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -945,7 +923,6 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -953,7 +930,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -961,13 +937,13 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -977,8 +953,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1037,8 +1013,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1145,8 +1119,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY= gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/router/accepter.go b/router/accepter.go index 5ceb144cc..6a1721ae8 100644 --- a/router/accepter.go +++ b/router/accepter.go @@ -67,6 +67,10 @@ func (self *ctrlChannelAcceptor) HandleGroupedUnderlay(underlay channel.Underlay return nil, err } + // Only now is the channel fully bound: AcceptCtrlChannel registered it in the first bind handler, but + // a later one can still refuse it. + self.router.ctrls.MarkChannelEstablished() + self.router.NotifyOfReconnect(listenerCtrlChan) return mc, nil } diff --git a/router/env/ctrl.go b/router/env/ctrl.go index 8d6971534..225136c17 100644 --- a/router/env/ctrl.go +++ b/router/env/ctrl.go @@ -20,7 +20,9 @@ import ( "sync/atomic" "time" + "github.com/michaelquigley/pfxlog" "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/config" "github.com/openziti/ziti/v2/common/ctrlchan" "github.com/openziti/channel/v4" @@ -47,7 +49,10 @@ func newNetworkCtrl(ch ctrlchan.CtrlChannel, address string, heartbeatOptions *H address: address, heartbeatOptions: heartbeatOptions, } - result.lastContact.Store(time.Now().UnixMilli()) + // Seed the response time so a channel is not judged unresponsive before its first heartbeat has had a + // chance to be answered. + result.lastRx = time.Now().UnixMilli() + result.lastContact.Store(result.lastRx) return result } @@ -149,6 +154,25 @@ func (self *networkCtrl) CheckHeartBeat() { } else { self.unresponsive.Store(false) } + + // Unresponsive alone only deprioritizes. A channel whose underlays are dead but undetected keeps its + // group and dials only additional underlays, which the controller refuses once its side is gone, so + // nothing re-establishes the group until the OS abandons the connection minutes later. + if timeout := self.heartbeatOptions.CloseUnresponsiveTimeout; timeout > 0 && self.timeSinceLastResponse() > timeout { + log := pfxlog.Logger(). + WithField("address", self.address). + WithField("timeSinceLastResponse", self.timeSinceLastResponse()) + log.Error("no heartbeat response from controller in time, closing control channel") + if err := self.ch.Close(); err != nil { + log.WithError(err).Error("error closing unresponsive control channel") + } + } +} + +// timeSinceLastResponse reports how long it has been since the controller last answered a heartbeat. It is +// only read on the heartbeat goroutine, which is also the only writer of lastRx. +func (self *networkCtrl) timeSinceLastResponse() time.Duration { + return time.Duration(time.Now().UnixMilli()-self.lastRx) * time.Millisecond } func (self *networkCtrl) IsConnected() bool { @@ -162,11 +186,18 @@ func NewDefaultHeartbeatOptions() *HeartbeatOptions { } } +// NewHeartbeatOptions returns the control channel heartbeat options described by options, overlaying +// this package's unresponsiveAfter setting. It rejects a cadence that cannot keep the channel up, +// since CheckHeartBeat tears the channel down once CloseUnresponsiveTimeout passes without a +// response. func NewHeartbeatOptions(options *channel.HeartbeatOptions) (*HeartbeatOptions, error) { unresponsiveAfter, err := options.GetDuration("unresponsiveAfter") if err != nil { return nil, err } + if err := config.ValidateHeartbeatOptions(options); err != nil { + return nil, err + } result := NewDefaultHeartbeatOptions() result.HeartbeatOptions = *options if unresponsiveAfter != nil { diff --git a/router/env/ctrl_heartbeat_test.go b/router/env/ctrl_heartbeat_test.go new file mode 100644 index 000000000..bdc61b6c6 --- /dev/null +++ b/router/env/ctrl_heartbeat_test.go @@ -0,0 +1,220 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package env + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/stretchr/testify/require" +) + +// heartbeatTestChannel is a ctrlchan.CtrlChannel double that records Close calls. It embeds the interface, +// so any method the heartbeat path does not use panics rather than silently returning a zero value. +type heartbeatTestChannel struct { + ctrlchan.CtrlChannel + closed atomic.Bool +} + +func (self *heartbeatTestChannel) Close() error { + self.closed.Store(true) + return nil +} + +func (self *heartbeatTestChannel) IsClosed() bool { return self.closed.Load() } + +func (self *heartbeatTestChannel) IsConnected() bool { return !self.closed.Load() } + +func newHeartbeatTestCtrl(t *testing.T) (*networkCtrl, *heartbeatTestChannel) { + t.Helper() + ch := &heartbeatTestChannel{} + options := NewDefaultHeartbeatOptions() + options.CloseUnresponsiveTimeout = time.Minute + options.UnresponsiveAfter = 5 * time.Second + return newNetworkCtrl(ch, "tls:localhost:6262", options), ch +} + +// TestCheckHeartBeat_ClosesWhenNoResponse covers the recovery path for a control channel whose peer has +// stopped answering. Until it is closed, the channel keeps its group and only ever dials additional +// underlays, which the controller refuses once it has torn its side of the group down, so the router can +// never re-establish it. +func TestCheckHeartBeat_ClosesWhenNoResponse(t *testing.T) { + ctrl, ch := newHeartbeatTestCtrl(t) + + // A fresh channel is not judged unresponsive before its first heartbeat could be answered. + ctrl.CheckHeartBeat() + require.False(t, ch.IsClosed(), "a channel that has just connected must not be closed") + + // No response for longer than the timeout. + ctrl.lastRx = time.Now().Add(-2 * time.Minute).UnixMilli() + ctrl.CheckHeartBeat() + require.True(t, ch.IsClosed(), "a channel with no heartbeat response in time must be closed") +} + +// TestCheckHeartBeat_SlowButAnsweringIsNotClosed guards the control channel's preference for staying up: +// high latency only deprioritizes a controller when choosing between them. Closing a channel that is +// merely slow would add control-plane downtime, so only a peer that has stopped answering entirely is +// torn down. +func TestCheckHeartBeat_SlowButAnsweringIsNotClosed(t *testing.T) { + ctrl, ch := newHeartbeatTestCtrl(t) + + // Answering, but slowly enough to count as unresponsive for selection purposes. + ctrl.lastRx = time.Now().UnixMilli() + ctrl.latency.Store(int64(30 * time.Second)) + + ctrl.CheckHeartBeat() + + require.True(t, ctrl.IsUnresponsive(), "high latency should mark the controller unresponsive") + require.False(t, ch.IsClosed(), "a channel that is still answering must not be closed") +} + +// TestCheckHeartBeat_ZeroTimeoutDisablesClose: a zero timeout turns the teardown off, for deployments that +// would rather keep a silent channel than have it recycled. +func TestCheckHeartBeat_ZeroTimeoutDisablesClose(t *testing.T) { + ctrl, ch := newHeartbeatTestCtrl(t) + ctrl.heartbeatOptions.CloseUnresponsiveTimeout = 0 + + ctrl.lastRx = time.Now().Add(-time.Hour).UnixMilli() + ctrl.CheckHeartBeat() + + require.False(t, ch.IsClosed(), "a zero close timeout must disable the teardown") +} + +// TestNewHeartbeatOptions_KeysLandInOwnFields covers the overlay this package puts on top of the channel +// heartbeat options. The close timeout is only meaningful if it survives config loading, and a value that +// lands in checkInterval instead makes the pulse sample a stale last-response time, which closes healthy +// channels rather than unresponsive ones. +func TestNewHeartbeatOptions_KeysLandInOwnFields(t *testing.T) { + load := func(t *testing.T, src map[interface{}]interface{}) *HeartbeatOptions { + t.Helper() + chOptions, err := channel.LoadHeartbeatOptions(src) + require.NoError(t, err) + options, err := NewHeartbeatOptions(chOptions) + require.NoError(t, err) + return options + } + + t.Run("each key lands in its own field", func(t *testing.T) { + options := load(t, map[interface{}]interface{}{ + "sendInterval": "3s", + "checkInterval": "4s", + "closeUnresponsiveTimeout": "5s", + "unresponsiveAfter": "6s", + }) + require.Equal(t, 3*time.Second, options.SendInterval) + require.Equal(t, 4*time.Second, options.CheckInterval) + require.Equal(t, 5*time.Second, options.CloseUnresponsiveTimeout) + require.Equal(t, 6*time.Second, options.UnresponsiveAfter) + }) + + t.Run("the close timeout leaves the check interval alone", func(t *testing.T) { + defaults := NewDefaultHeartbeatOptions() + options := load(t, map[interface{}]interface{}{ + "closeUnresponsiveTimeout": "90s", + }) + require.Equal(t, 90*time.Second, options.CloseUnresponsiveTimeout) + require.Equal(t, defaults.CheckInterval, options.CheckInterval) + require.Equal(t, defaults.UnresponsiveAfter, options.UnresponsiveAfter) + }) + + t.Run("the teardown can be disabled from config", func(t *testing.T) { + options := load(t, map[interface{}]interface{}{ + "closeUnresponsiveTimeout": "0s", + }) + require.Zero(t, options.CloseUnresponsiveTimeout) + require.Positive(t, options.CheckInterval, "a zero close timeout must not zero the check interval") + }) + + t.Run("defaults apply when nothing is set", func(t *testing.T) { + defaults := NewDefaultHeartbeatOptions() + options := load(t, map[interface{}]interface{}{}) + require.Equal(t, defaults.SendInterval, options.SendInterval) + require.Equal(t, defaults.CheckInterval, options.CheckInterval) + require.Equal(t, defaults.CloseUnresponsiveTimeout, options.CloseUnresponsiveTimeout) + require.Equal(t, defaults.UnresponsiveAfter, options.UnresponsiveAfter) + }) +} + +// TestNewHeartbeatOptions_RejectsUnworkableCadence covers the cadence relation the loader enforces. +// CheckHeartBeat only runs from the heartbeat pulse, so the check interval is the sampling rate for +// the close timeout: at or above it the check reads a response time up to a full interval stale and +// tears down a controller that is answering, which then reconnects and repeats. +func TestNewHeartbeatOptions_RejectsUnworkableCadence(t *testing.T) { + load := func(src map[interface{}]interface{}) (*HeartbeatOptions, error) { + chOptions, err := channel.LoadHeartbeatOptions(src) + require.NoError(t, err) + return NewHeartbeatOptions(chOptions) + } + + // The close timeout defaults to 30s, so raising only the check interval is enough to break it. + t.Run("a check interval above the default timeout is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "checkInterval") + }) + + t.Run("a check interval above an explicit timeout is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + "closeUnresponsiveTimeout": "30s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "checkInterval") + }) + + t.Run("a send interval above the timeout is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "sendInterval": "60s", + "closeUnresponsiveTimeout": "30s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "sendInterval") + }) + + t.Run("a zero check interval is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "checkInterval": "0s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "checkInterval") + }) + + t.Run("widening the timeout alongside the check interval is accepted", func(t *testing.T) { + options, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + "sendInterval": "60s", + "closeUnresponsiveTimeout": "5m", + }) + require.NoError(t, err) + require.Equal(t, time.Minute, options.CheckInterval) + require.Equal(t, 5*time.Minute, options.CloseUnresponsiveTimeout) + }) + + t.Run("disabling the teardown accepts any positive check interval", func(t *testing.T) { + options, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + "closeUnresponsiveTimeout": "0s", + }) + require.NoError(t, err) + require.Zero(t, options.CloseUnresponsiveTimeout) + }) +} diff --git a/router/env/ctrls.go b/router/env/ctrls.go index 4e73425a2..5edaaaafd 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -72,6 +72,8 @@ type DialEnv interface { } type NetworkControllers interface { + MarkChannelEstablished() + EverConnected() bool GetControllerDetails() map[string]*ctrl_pb.CtrlDetail UpdateControllerDetails(controllers []*ctrl_pb.CtrlDetail) bool ConnectToInitialEndpoints(endpoints []string) @@ -120,6 +122,27 @@ type networkControllers struct { leaderId concurrenz.AtomicValue[string] ctrlChangeListeners concurrenz.CopyOnWriteSlice[CtrlEventListener] controllerDetails concurrenz.AtomicValue[map[string]*ctrl_pb.CtrlDetail] + closed atomic.Bool + // everConnected records that a control channel was established at some point. It is never cleared, so it + // answers whether the router has ever reached a controller rather than whether it is reachable now. + everConnected atomic.Bool +} + +// MarkChannelEstablished records that a control channel was fully established. It must be called only +// once the whole bind has succeeded, not when the registration is created: Add runs in the first of +// several bind handlers, and a later one can still refuse the channel, at which point it is closed and +// unregistered. Setting this from Add would mean a router that only ever reached a controller it cannot +// talk to, an older one lacking a required capability for instance, counted as having connected, and so +// never failed its startup check and never exited. +func (self *networkControllers) MarkChannelEstablished() { + self.everConnected.Store(true) +} + +// EverConnected reports whether this router has established a control channel to any controller since +// starting. It stays true once set, so unlike the current controller count it is not affected by a router +// being momentarily between channels. +func (self *networkControllers) EverConnected() bool { + return self.everConnected.Load() } func (self *networkControllers) ControllersHaveMinVersion(version string) bool { @@ -176,9 +199,9 @@ func (self *networkControllers) UpdateControllerDetails(controllers []*ctrl_pb.C } } - // Start dialing new controllers that aren't already dialing or connected + // Start dialing new controllers that aren't already dialing or usably connected for ctrlId, detail := range newIdSet { - if !self.idsBeingDialed.Has(ctrlId) && self.ctrls.Get(ctrlId) == nil { + if !self.idsBeingDialed.Has(ctrlId) && !isUsable(self.ctrls.Get(ctrlId)) { log.WithField("ctrlId", ctrlId).WithField("endpoints", detail.Endpoints).Info("adding new ctrl") changed = true self.connectToControllerWithBackoff(detail) @@ -215,6 +238,11 @@ func (self *networkControllers) getControllerDetail(controllerId string) *ctrl_p func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.CtrlDetail) { log := pfxlog.Logger().WithField("ctrlId", detail.Id).WithField("detail", detail) + if self.closed.Load() { + log.Info("network controllers closed, not dialing controller") + return + } + if len(detail.Endpoints) == 0 { log.Error("controller has no endpoints, unable to connect") return @@ -232,12 +260,17 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C idx := 0 operation := func() error { + if self.closed.Load() { + return backoff.Permanent(errors.New("network controllers closed")) + } + if detail.Id != "" && !self.idsBeingDialed.Has(detail.Id) { return backoff.Permanent(errors.New("controller removed before connection established")) } - // Already connected by ID - if self.ctrls.Get(detail.Id) != nil { + // Already connected by id. An unusable registration is not a reason to stop: the dial displaces it, + // where treating it as connected would end the retries and leave the router holding nothing. + if isUsable(self.ctrls.Get(detail.Id)) { log.Info("already connected to controller, exiting retry") return nil } @@ -245,7 +278,7 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C // Already connected by address for _, ep := range detail.Endpoints { for _, v := range self.ctrls.AsMap() { - if v.Address() == ep.Address { + if v.Address() == ep.Address && isUsable(v) { log.WithField("endpoint", ep.Address).Info("already connected to controller by address, exiting retry") return nil } @@ -279,6 +312,11 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C err = self.connectToController(ep.Address, addr) if err != nil { + if ctrlId, ok := self.duplicateResolved(err); ok { + log.WithField("endpoint", ep.Address).WithField("ctrlId", ctrlId). + Info("endpoint reached an already connected controller, exiting retry") + return nil + } log.WithField("endpoint", ep.Address).WithError(err).Error("unable to connect controller") } return err @@ -299,6 +337,18 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C }() } +// firstUnderlayHeaders returns a copy of the grouped-channel base headers marked with +// IsFirstGroupConnection. Only the initial underlay carries this flag; additional underlays reuse the +// unmarked base headers so the controller does not treat each of them as a new channel. +func firstUnderlayHeaders(base channel.Headers) channel.Headers { + first := channel.Headers{} + for k, v := range base { + first[k] = v + } + first.PutBoolHeader(channel.IsFirstGroupConnection, true) + return first +} + func (self *networkControllers) connectToController(endpoint string, addr transport.Address) error { headers, err := self.dialEnv.GetChannelHeaders() if err != nil { @@ -311,10 +361,12 @@ func (self *networkControllers) connectToController(endpoint string, addr transp logrus.Debugf("Using local interface %s to dial controller", config.Ctrl.LocalBinding) } - // Build headers for the initial dial, including grouped channel flags + // Base headers apply to every underlay of the grouped channel. IsFirstGroupConnection must NOT be + // set here: the dialer reuses these headers when creating additional (e.g. ctrl.high) underlays, and + // an inherited first-connection flag would make each additional underlay look like a new channel and + // trip the controller's already-connected guard. headers.PutBoolHeader(channel.IsGroupedHeader, true) headers.PutStringHeader(channel.TypeHeader, ctrlchan.ChannelTypeDefault) - headers.PutBoolHeader(channel.IsFirstGroupConnection, true) dialer := channel.NewClassicDialer(channel.DialerConfig{ Identity: config.Id, @@ -327,8 +379,11 @@ func (self *networkControllers) connectToController(endpoint string, addr transp }, }) + // The initial underlay, and only it, carries IsFirstGroupConnection. + firstDialHeaders := firstUnderlayHeaders(headers) + // Dial initial underlay - underlay, err := dialer.CreateWithHeaders(config.Ctrl.Options.ConnectTimeout, headers) + underlay, err := dialer.CreateWithHeaders(config.Ctrl.Options.ConnectTimeout, firstDialHeaders) if err != nil { return fmt.Errorf("error connecting ctrl (%v)", err) } @@ -341,19 +396,14 @@ func (self *networkControllers) connectToController(endpoint string, addr transp // Track connectivity transitions for reconnect/disconnect notifications var wasDisconnected atomic.Bool - changeCallback := func(ch *ctrlchan.DialCtrlChannel, oldCount, newCount uint32) { + changeCallback := func(ch *ctrlchan.DialCtrlChannel, _, newCount uint32) { multiCh := ch.GetChannel() if multiCh == nil || multiCh.IsClosed() { return } - if wasDisconnected.Load() && newCount > 0 { + self.notifyOfConnectivityChange(ch.PeerId(), &wasDisconnected, newCount, func() { self.dialEnv.NotifyOfReconnect(ch) - wasDisconnected.Store(false) - } else if newCount == 0 { - if wasDisconnected.CompareAndSwap(false, true) { - self.NotifyOfDisconnect(ch.PeerId()) - } - } + }) } dialCtrlChan := ctrlchan.NewDialCtrlChannel(ctrlchan.DialCtrlChannelConfig{ @@ -369,21 +419,13 @@ func (self *networkControllers) connectToController(endpoint string, addr transp id := binding.GetChannel().Id() binding.AddReceiveHandlerF(int32(edge_ctrl_pb.ContentType_CurrentIndexMessageType), self.handleRouterDataModelIndexUpdate) - if err = self.Add(endpoint, dialCtrlChan, binding.GetChannel(), underlay); err != nil { + ctrl, err := self.Add(endpoint, dialCtrlChan, binding.GetChannel(), underlay) + if err != nil { return err } - binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) { - ctrl := self.GetNetworkController(id) - self.ctrls.Delete(id) - if ctrl != nil { - self.notifyOfChange(ctrl, ControllerDisconnected) - } - if detail := self.getControllerDetail(id); detail != nil { - time.AfterFunc(time.Second, func() { - self.connectToControllerWithBackoff(detail) - }) - } + binding.AddCloseHandler(channel.CloseHandlerF(func(channel.Channel) { + self.handleChannelClose(id, ctrl, time.Second) })) return nil @@ -411,6 +453,8 @@ func (self *networkControllers) connectToController(endpoint string, addr transp return fmt.Errorf("error connecting ctrl (%w)", err) } + self.MarkChannelEstablished() + // If there are multiple controllers we may have to catch up the controllers that connected later // with things that have already happened because we had state from other controllers, such as // links @@ -427,60 +471,173 @@ func (self *networkControllers) handleRouterDataModelIndexUpdate(m *channel.Mess } } -func (self *networkControllers) Add(address string, ctrlCh ctrlchan.CtrlChannel, ch channel.Channel, underlay channel.Underlay) error { +// Add registers a newly established control channel and returns the entry created for it. The returned +// entry is the identity a close handler must present to give the registration up again, so callers that +// wire a close handler have to hold on to it. +func (self *networkControllers) Add(address string, ctrlCh ctrlchan.CtrlChannel, ch channel.Channel, underlay channel.Underlay) (NetworkController, error) { ctrl := newNetworkCtrl(ctrlCh, address, self.heartbeatOptions) if versionValue, found := underlay.Headers()[channel.HelloVersionHeader]; found { if versionInfo, err := versions.StdVersionEncDec.Decode(versionValue); err == nil { ctrl.versionInfo = versionInfo } else { - return fmt.Errorf("could not parse version info from controller hello, closing connection (%w)", err) + return nil, fmt.Errorf("could not parse version info from controller hello, closing connection (%w)", err) } } else { - return errors.New("no version header provided") + return nil, errors.New("no version header provided") } - if existing := self.ctrls.Get(ch.Id()); existing != nil { - if !existing.Channel().IsClosed() && existing.IsConnected() { - // if an existing channel exists and is connected, reject the duplicate - return backoff.Permanent(fmt.Errorf("duplicate channel with id %v", ctrl.Channel().Id())) - } - // existing channel is closed or disconnected (0 underlays) — close it and accept new one - if !existing.Channel().IsClosed() { - if closeErr := existing.Channel().Close(); closeErr != nil { - pfxlog.Logger().WithError(closeErr).WithField("ch", existing.Channel().Label()).Error("error closing control channel") - } + log := pfxlog.Logger(). + WithField("ctrlId", ch.Id()). + WithField("ch", ch.Label()). + WithField("address", address) + + // Atomic against a concurrent Add for the same controller and against UpdateControllerDetails, which + // decides whether to dial from the same state. Two dials do race: initial endpoint dials carry no + // controller id, so idsBeingDialed cannot keep them apart. + self.lock.Lock() + + existing := self.ctrls.Get(ch.Id()) + if isUsable(existing) { + self.lock.Unlock() + return nil, &errDuplicateChannel{ctrlId: ch.Id()} + } + + // Hand the registration over before closing what it displaced, so the displaced close handler finds + // itself superseded rather than observing an empty registration and racing this dial with a redial. + self.ctrls.Put(ch.Id(), ctrl) + + self.lock.Unlock() + + log.Info("controller registered") + + // Closing a channel runs its close handlers on this goroutine, so it must happen outside the lock. + if existing != nil && !existing.Channel().IsClosed() { + if closeErr := existing.Channel().Close(); closeErr != nil { + log.WithError(closeErr).WithField("displacedCh", existing.Channel().Label()). + Error("error closing displaced control channel") } } - self.ctrls.Put(ch.Id(), ctrl) self.notifyOfChange(ctrl, ControllerAdded) - return nil + return ctrl, nil +} + +// errDuplicateChannel reports that a control channel was refused because the controller it reached is +// already usably connected. It carries the controller's id because a dial started from a bare endpoint has +// none of its own, so this is the only way the retry loop can learn which controller the endpoint resolved +// to and stop dialing it. +type errDuplicateChannel struct { + ctrlId string +} + +func (self *errDuplicateChannel) Error() string { + return fmt.Sprintf("controller %v is already connected on another channel", self.ctrlId) +} + +// duplicateResolved reports the controller id when err is a refusal by a controller whose registration is +// still usable, meaning another channel already did what this dial set out to do. A dial that only reaches +// an endpoint learns the controller's id here and nowhere else, which is why the refusal has to carry it. +// The registration is rechecked because the channel that won the race may since have died, leaving the +// dial's work to do after all. +func (self *networkControllers) duplicateResolved(err error) (string, bool) { + var duplicate *errDuplicateChannel + if errors.As(err, &duplicate) && isUsable(self.ctrls.Get(duplicate.ctrlId)) { + return duplicate.ctrlId, true + } + return "", false +} + +// isUsable reports whether a controller registration can still carry traffic. A registration whose channel +// has been closed, or which has lost every underlay, satisfies a presence check while carrying nothing. +// Every decision about whether the router is connected to a controller has to ask this rather than whether +// a registration exists, or an entry left behind by a channel that died leaves the router believing it is +// connected and suppresses the reconnect that would fix it. +func isUsable(ctrl NetworkController) bool { + return ctrl != nil && !ctrl.CtrlChannel().IsClosed() && ctrl.IsConnected() +} + +// removeIfCurrent gives up ctrl's registration for ctrlId, and reports whether it was still the +// registered entry. Overlapping channels to one controller share its id, so removing by id alone lets a +// superseded channel's close delete its replacement's registration. Nothing re-registers a channel that +// is already established, so the surviving channel would stay unreachable through the map and the router +// would be invisible to that controller until the channel happened to close. +func (self *networkControllers) removeIfCurrent(ctrlId string, ctrl NetworkController) bool { + return self.ctrls.DeleteIf(func(key string, val NetworkController) bool { + return key == ctrlId && val == ctrl + }) +} + +// handleChannelClose releases ctrl's registration and starts reconnecting to the controller. It is a +// no-op when ctrl is no longer the registered entry, either because a newer channel has taken over or +// because the controller was removed from the cluster. Reconnects are delayed by redialDelay, which +// paces a dial the controller rejects outright. +func (self *networkControllers) handleChannelClose(ctrlId string, ctrl NetworkController, redialDelay time.Duration) { + log := pfxlog.Logger().WithField("ctrlId", ctrlId).WithField("ch", ctrl.Channel().Label()) + + if !self.removeIfCurrent(ctrlId, ctrl) { + log.Info("superseded control channel closed, leaving the current registration in place") + return + } + + log.Info("control channel closed, controller unregistered") + self.notifyOfChange(ctrl, ControllerDisconnected) + + detail := self.getControllerDetail(ctrlId) + if detail == nil { + log.Info("controller is no longer known, not reconnecting") + return + } + + if redialDelay > 0 { + time.AfterFunc(redialDelay, func() { + self.connectToControllerWithBackoff(detail) + }) + } else { + self.connectToControllerWithBackoff(detail) + } } func (self *networkControllers) AcceptCtrlChannel(address string, ctrlCh ctrlchan.CtrlChannel, binding channel.Binding, underlay channel.Underlay) error { id := binding.GetChannel().Id() binding.AddReceiveHandlerF(int32(edge_ctrl_pb.ContentType_CurrentIndexMessageType), self.handleRouterDataModelIndexUpdate) - if err := self.Add(address, ctrlCh, binding.GetChannel(), underlay); err != nil { + ctrl, err := self.Add(address, ctrlCh, binding.GetChannel(), underlay) + if err != nil { return err } - binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) { - ctrl := self.GetNetworkController(id) - self.ctrls.Delete(id) - if ctrl != nil { - self.notifyOfChange(ctrl, ControllerDisconnected) - } - if detail := self.getControllerDetail(id); detail != nil { - self.connectToControllerWithBackoff(detail) - } + binding.AddCloseHandler(channel.CloseHandlerF(func(channel.Channel) { + self.handleChannelClose(id, ctrl, 0) })) return nil } +// notifyOfConnectivityChange reports a control channel losing and regaining its last +// underlay. A grouped channel survives losing every underlay, so the registration stays +// and neither Add nor the close handler runs: these notifications are the only thing that +// tells anything the channel went away and came back. +// +// Both halves of the reconnect matter and are easy to confuse. notifyReconnect re-offers +// the state a controller may have missed while the channel was down, while the +// ControllerReconnected ctrl event is what listeners keyed on connectivity wait for. +// Sending only the first leaves those listeners believing the controller is still gone. +// +// wasDisconnected belongs to the one channel and is swapped rather than stored, so each +// edge is reported exactly once however many underlays come or go at the same moment. +func (self *networkControllers) notifyOfConnectivityChange(ctrlId string, wasDisconnected *atomic.Bool, newCount uint32, notifyReconnect func()) { + if newCount > 0 { + if wasDisconnected.CompareAndSwap(true, false) { + notifyReconnect() + self.NotifyOfReconnect(ctrlId) + } + } else if wasDisconnected.CompareAndSwap(false, true) { + self.NotifyOfDisconnect(ctrlId) + } +} + func (self *networkControllers) NotifyOfDisconnect(ctrlId string) { if ctrl := self.GetNetworkController(ctrlId); ctrl != nil { self.notifyOfChange(ctrl, ControllerDisconnected) @@ -611,6 +768,7 @@ func (self *networkControllers) ForEach(f func(controllerId string, ch channel.C } func (self *networkControllers) Close() error { + self.closed.Store(true) self.idsBeingDialed.Clear() var errList []error self.ForEach(func(_ string, ch channel.Channel) { diff --git a/router/env/ctrls_mock.go b/router/env/ctrls_mock.go index 4938ae327..ef1a2ec32 100644 --- a/router/env/ctrls_mock.go +++ b/router/env/ctrls_mock.go @@ -50,6 +50,9 @@ func (m *MockNetworkControllers) UpdateControllerDetails(controllers []*ctrl_pb. return false } +func (m *MockNetworkControllers) MarkChannelEstablished() {} +func (m *MockNetworkControllers) EverConnected() bool { return true } + func (m *MockNetworkControllers) ConnectToInitialEndpoints(endpoints []string) { } diff --git a/router/env/ctrls_test.go b/router/env/ctrls_test.go new file mode 100644 index 000000000..baad3d771 --- /dev/null +++ b/router/env/ctrls_test.go @@ -0,0 +1,560 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package env + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + + "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/pb/ctrl_pb" + + cmap "github.com/orcaman/concurrent-map/v2" + + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/stretchr/testify/require" +) + +// Test_firstUnderlayHeaders verifies that only the initial underlay is flagged as the first grouped +// connection. If the flag leaked onto the base headers, every additional underlay (e.g. ctrl.high) +// would look like a new channel and trip the controller's already-connected churn guard. +func Test_firstUnderlayHeaders(t *testing.T) { + req := require.New(t) + + base := channel.Headers{} + base.PutBoolHeader(channel.IsGroupedHeader, true) + base.PutStringHeader(channel.TypeHeader, ctrlchan.ChannelTypeDefault) + + first := firstUnderlayHeaders(base) + + // The base headers, reused for additional underlays, must not carry the first-connection flag. + _, baseHasFirst := base[channel.IsFirstGroupConnection] + req.False(baseHasFirst, "base headers for additional underlays must not be flagged as first") + + // The initial underlay's headers must carry it. + firstFlag, ok := first.GetBoolHeader(channel.IsFirstGroupConnection) + req.True(ok, "initial underlay headers must contain the first-connection flag") + req.True(firstFlag) + + // The remaining base headers are preserved on the copy. + grouped, _ := first.GetBoolHeader(channel.IsGroupedHeader) + req.True(grouped, "grouped flag preserved on the initial underlay copy") + chType, _ := first.GetStringHeader(channel.TypeHeader) + req.Equal(ctrlchan.ChannelTypeDefault, chType, "channel type preserved on the initial underlay copy") +} + +// ctrlsTestChannel is a channel.Channel double reporting a fixed id and label. It embeds the interface, so +// any method these tests do not exercise panics rather than silently returning a zero value. +type ctrlsTestChannel struct { + channel.Channel + id string + label string + closed atomic.Bool + // underlaysLost models a channel that has lost every underlay without having been closed, which is the + // state Add treats as displaceable. + underlaysLost atomic.Bool +} + +func (self *ctrlsTestChannel) Id() string { return self.id } +func (self *ctrlsTestChannel) Label() string { return self.label } +func (self *ctrlsTestChannel) IsClosed() bool { return self.closed.Load() } +func (self *ctrlsTestChannel) Close() error { self.closed.Store(true); return nil } +func (self *ctrlsTestChannel) IsConnected() bool { + return !self.closed.Load() && !self.underlaysLost.Load() +} + +// ctrlsTestCtrlChannel is a ctrlchan.CtrlChannel double wrapping a ctrlsTestChannel. +type ctrlsTestCtrlChannel struct { + ctrlchan.CtrlChannel + ch *ctrlsTestChannel +} + +func (self *ctrlsTestCtrlChannel) GetChannel() channel.Channel { return self.ch } +func (self *ctrlsTestCtrlChannel) IsClosed() bool { return self.ch.IsClosed() } +func (self *ctrlsTestCtrlChannel) IsConnected() bool { return self.ch.IsConnected() } +func (self *ctrlsTestCtrlChannel) Close() error { return self.ch.Close() } + +// ctrlsTestUnderlay is a channel.Underlay double supplying only the hello headers Add reads. +type ctrlsTestUnderlay struct { + channel.Underlay + headers channel.Headers +} + +func (self *ctrlsTestUnderlay) Headers() map[int32][]byte { return self.headers } + +// newTestUnderlay supplies the version header Add requires of a controller hello. +func newTestUnderlay(t *testing.T) channel.Underlay { + t.Helper() + encoded, err := versions.StdVersionEncDec.Encode(&versions.VersionInfo{Version: "v1.0.0"}) + require.NoError(t, err) + return &ctrlsTestUnderlay{headers: channel.Headers{channel.HelloVersionHeader: encoded}} +} + +// newTestNetworkControllers builds a networkControllers with no DialEnv. Tests using it must not reach a +// path that dials, which for these tests means leaving controllerDetails empty so no reconnect is started. +func newTestNetworkControllers() *networkControllers { + return &networkControllers{ + heartbeatOptions: NewDefaultHeartbeatOptions(), + idsBeingDialed: cmap.New[struct{}](), + } +} + +// newTestNetworkCtrl returns a registration and the test channel backing it, so tests can drive the +// channel into the states the usability check distinguishes. +func newTestNetworkCtrl(nc *networkControllers, ctrlId string, label string) (*networkCtrl, *ctrlsTestChannel) { + ch := &ctrlsTestChannel{id: ctrlId, label: label} + return newNetworkCtrl(&ctrlsTestCtrlChannel{ch: ch}, "tls:"+ctrlId+":6262", nc.heartbeatOptions), ch +} + +// collectCtrlEvents registers a listener and returns a function draining the events seen so far. Listeners +// are notified on their own goroutine, so the buffer carries them back to the test. +func collectCtrlEvents(nc *networkControllers) func() []CtrlEvent { + received := make(chan CtrlEvent, 16) + nc.AddChangeListener(CtrlEventListenerFunc(func(event CtrlEvent) { + received <- event + })) + + return func() []CtrlEvent { + var result []CtrlEvent + for { + select { + case e := <-received: + result = append(result, e) + default: + return result + } + } + } +} + +// TestHandleChannelClose_SupersededLeavesCurrentRegistered guards against a router going permanently +// invisible to a controller. Overlapping channels to one controller share its id, so a close that gives up +// the registration by id alone can delete the live channel's entry. Nothing re-registers an already +// established channel, and the reconnect path treats a present entry as connected, so the router would +// neither notice nor redial. +func TestHandleChannelClose_SupersededLeavesCurrentRegistered(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + superseded, _ := newTestNetworkCtrl(nc, "ctrl1", "old") + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + + nc.ctrls.Put("ctrl1", current) + + // The superseded channel's close lands after its replacement has registered. + nc.handleChannelClose("ctrl1", superseded, 0) + + require.Same(t, current, nc.ctrls.Get("ctrl1"), + "a superseded channel's close must not unregister the channel that replaced it") + + require.Never(t, func() bool { + return len(drain()) > 0 + }, 100*time.Millisecond, 10*time.Millisecond, + "a superseded channel's close must not report the current channel as disconnected") +} + +// TestHandleChannelClose_CurrentUnregisters covers the ordinary case: the registered channel closing gives +// up its registration and reports the disconnect. +func TestHandleChannelClose_CurrentUnregisters(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", current) + + nc.handleChannelClose("ctrl1", current, 0) + + require.Nil(t, nc.ctrls.Get("ctrl1"), "the registered channel closing must unregister the controller") + + var events []CtrlEvent + require.Eventually(t, func() bool { + events = append(events, drain()...) + return len(events) > 0 + }, time.Second, 10*time.Millisecond, "expected a controller change event") + + require.Len(t, events, 1) + require.Equal(t, ControllerDisconnected, events[0].Type) + require.Same(t, current, events[0].Controller) +} + +// TestHandleChannelClose_AlreadyUnregistered: closeAndRemoveById drops the entry before closing the +// channel, so the close handler that follows must not report a second disconnect. +func TestHandleChannelClose_AlreadyUnregistered(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + + nc.handleChannelClose("ctrl1", current, 0) + + require.Never(t, func() bool { + return len(drain()) > 0 + }, 100*time.Millisecond, 10*time.Millisecond, + "closing a channel that is already unregistered must not report a disconnect") +} + +// TestRemoveIfCurrent_LeavesOtherControllers guards the key comparison: giving up one controller's +// registration must not touch another's. +func TestRemoveIfCurrent_LeavesOtherControllers(t *testing.T) { + nc := newTestNetworkControllers() + + ctrl1, _ := newTestNetworkCtrl(nc, "ctrl1", "ctrl1") + ctrl2, _ := newTestNetworkCtrl(nc, "ctrl2", "ctrl2") + nc.ctrls.Put("ctrl1", ctrl1) + nc.ctrls.Put("ctrl2", ctrl2) + + require.True(t, nc.removeIfCurrent("ctrl1", ctrl1)) + + require.Nil(t, nc.ctrls.Get("ctrl1")) + require.Same(t, ctrl2, nc.ctrls.Get("ctrl2")) + + require.False(t, nc.removeIfCurrent("ctrl1", ctrl1), "removing an absent registration must report no match") +} + +// TestIsUsable covers what counts as being connected to a controller. A registration is not evidence of +// connectivity on its own: the channel behind it may have been closed, or may have lost every underlay +// without closing, which is the state a control channel is left in when its group dies but nothing detects +// it. +func TestIsUsable(t *testing.T) { + nc := newTestNetworkControllers() + + require.False(t, isUsable(nil), "an absent registration is not usable") + + healthy, _ := newTestNetworkCtrl(nc, "ctrl1", "healthy") + require.True(t, isUsable(healthy)) + + closed, closedCh := newTestNetworkCtrl(nc, "ctrl2", "closed") + closedCh.closed.Store(true) + require.False(t, isUsable(closed), "a registration whose channel is closed is not usable") + + underlayless, underlaylessCh := newTestNetworkCtrl(nc, "ctrl3", "no-underlays") + underlaylessCh.underlaysLost.Store(true) + require.False(t, isUsable(underlayless), "a registration with no underlays left is not usable") +} + +// TestUpdateControllerDetails_ReconnectsWhenRegistrationUnusable: a registration left behind by a channel +// that died must not suppress the reconnect. Treating presence as connectivity is what leaves a router +// permanently invisible to a controller, since nothing else in this path would notice. +func TestUpdateControllerDetails_ReconnectsWhenRegistrationUnusable(t *testing.T) { + nc := newTestNetworkControllers() + + stale, staleCh := newTestNetworkCtrl(nc, "ctrl1", "stale") + staleCh.underlaysLost.Store(true) + nc.ctrls.Put("ctrl1", stale) + + // The detail carries no endpoints, so the reconnect gives up before reaching the network. + require.True(t, nc.UpdateControllerDetails([]*ctrl_pb.CtrlDetail{{Id: "ctrl1"}}), + "an unusable registration must not suppress the reconnect") +} + +// TestUpdateControllerDetails_LeavesUsableRegistrationAlone is the other half: a working channel must not +// be redialed on every controller detail update. +func TestUpdateControllerDetails_LeavesUsableRegistrationAlone(t *testing.T) { + nc := newTestNetworkControllers() + + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", current) + + require.False(t, nc.UpdateControllerDetails([]*ctrl_pb.CtrlDetail{{Id: "ctrl1"}}), + "a usable registration must not be redialed") + require.Same(t, current, nc.ctrls.Get("ctrl1")) +} + +// TestAdd_RejectsDuplicateOfUsableChannel: one usable channel per controller. The duplicate is refused and +// the established channel is left alone. +func TestAdd_RejectsDuplicateOfUsableChannel(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + established := &ctrlsTestChannel{id: "ctrl1", label: "established"} + first, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: established}, established, underlay) + require.NoError(t, err) + + duplicate := &ctrlsTestChannel{id: "ctrl1", label: "duplicate"} + second, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: duplicate}, duplicate, underlay) + require.Error(t, err) + require.Nil(t, second) + + require.Same(t, first, nc.ctrls.Get("ctrl1"), "the established channel must keep its registration") + require.False(t, established.IsClosed(), "the established channel must not be closed to admit a duplicate") +} + +// TestAdd_DuplicateErrorIsRetryable: losing a dial race is an ordinary race, not a permanent condition. A +// permanent refusal ends the retry loop for that controller for the life of the process, so if the winner +// later turns out to be unusable there is nothing left to redial. +func TestAdd_DuplicateErrorIsRetryable(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + established := &ctrlsTestChannel{id: "ctrl1", label: "established"} + _, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: established}, established, underlay) + require.NoError(t, err) + + duplicate := &ctrlsTestChannel{id: "ctrl1", label: "duplicate"} + _, err = nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: duplicate}, duplicate, underlay) + require.Error(t, err) + + var permanent *backoff.PermanentError + require.False(t, errors.As(err, &permanent), "a lost dial race must not stop the retry loop") + + var dup *errDuplicateChannel + require.True(t, errors.As(err, &dup), "the refusal must identify itself as a duplicate") + require.Equal(t, "ctrl1", dup.ctrlId, + "the refusal must carry the controller id, which a dial started from a bare endpoint has no other way to learn") +} + +// TestDuplicateResolved covers how the retry loop reacts to losing a race. The id checks at the top of a +// retry cannot help a dial started from a bare endpoint, since it has no controller id to check with, so +// the refusal itself is what ends the retries. +func TestDuplicateResolved(t *testing.T) { + nc := newTestNetworkControllers() + + winner, winnerCh := newTestNetworkCtrl(nc, "ctrl1", "winner") + nc.ctrls.Put("ctrl1", winner) + + // Wrapped the way connectToController wraps a dial failure. + refusal := fmt.Errorf("error connecting ctrl (%w)", &errDuplicateChannel{ctrlId: "ctrl1"}) + + ctrlId, ok := nc.duplicateResolved(refusal) + require.True(t, ok, "a refusal by a usably connected controller leaves this dial nothing to do") + require.Equal(t, "ctrl1", ctrlId) + + // The channel that won the race then dies, so the work is this dial's after all. + winnerCh.closed.Store(true) + _, ok = nc.duplicateResolved(refusal) + require.False(t, ok, "a refusal by a controller that is no longer usable must not end the retries") + + _, ok = nc.duplicateResolved(errors.New("connection refused")) + require.False(t, ok, "an unrelated failure must not be read as a lost race") +} + +// TestAdd_DisplacesUnusableChannel: a registration whose channel has no underlays left is not a reason to +// refuse a new one. The new channel takes the registration over and the old one is closed. +func TestAdd_DisplacesUnusableChannel(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + stale := &ctrlsTestChannel{id: "ctrl1", label: "stale"} + staleCtrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: stale}, stale, underlay) + require.NoError(t, err) + + // Simulate the channel losing its underlays without having been closed. + stale.underlaysLost.Store(true) + + replacement := &ctrlsTestChannel{id: "ctrl1", label: "replacement"} + replacementCtrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: replacement}, replacement, underlay) + require.NoError(t, err) + require.NotSame(t, staleCtrl, replacementCtrl) + + require.Same(t, replacementCtrl, nc.ctrls.Get("ctrl1"), "the new channel must hold the registration") + require.True(t, stale.IsClosed(), "the displaced channel must be closed") +} + +// TestAdd_ConcurrentForOneControllerRegistersOne is the check-then-put race. Initial endpoint dials carry +// no controller id, so idsBeingDialed cannot keep two dials to one controller apart; without the +// registration decision being atomic, both see no entry, both register, and whichever loses is left with a +// live channel that nothing tracks or reconnects. +func TestAdd_ConcurrentForOneControllerRegistersOne(t *testing.T) { + for i := 0; i < 50; i++ { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + const dials = 8 + start := make(chan struct{}) + var wg sync.WaitGroup + var registered atomic.Int32 + results := make([]NetworkController, dials) + + for j := 0; j < dials; j++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + ch := &ctrlsTestChannel{id: "ctrl1", label: fmt.Sprintf("dial-%d", idx)} + <-start + ctrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: ch}, ch, underlay) + if err == nil { + results[idx] = ctrl + registered.Add(1) + } + }(j) + } + + close(start) + wg.Wait() + + require.Equal(t, int32(1), registered.Load(), + "exactly one concurrent dial to a controller may take the registration") + + var winner NetworkController + for _, ctrl := range results { + if ctrl != nil { + winner = ctrl + } + } + require.Same(t, winner, nc.ctrls.Get("ctrl1"), "the registration must belong to the dial that took it") + } +} + +// TestEverConnected_NotSetByRegistrationAlone guards the difference between registering a control channel +// and having established one. Add runs in the first of several bind handlers, and a later one can still +// refuse the channel, after which it is closed and unregistered. If registering were enough to count, a +// router that only ever reached a controller it cannot talk to, an older one lacking a required capability +// for instance, would pass its startup check and retry forever instead of exiting. +func TestEverConnected_NotSetByRegistrationAlone(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + require.False(t, nc.EverConnected(), "a router that has not connected yet must report so") + + ch := &ctrlsTestChannel{id: "ctrl1", label: "first"} + ctrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: ch}, ch, underlay) + require.NoError(t, err) + require.False(t, nc.EverConnected(), + "a registration is not an established channel; the bind can still be refused after Add returns") + + // Losing it without the bind ever completing must leave the router still having never connected, so + // the startup check can do its job. + nc.handleChannelClose("ctrl1", ctrl, 0) + require.False(t, nc.EverConnected(), "a channel that was never established must not count as one") +} + +// TestEverConnected_StaysTrueAcrossLosingEveryController is the distinction the startup check depends on. +// The check fires once, so asking whether a controller is reachable at that instant kills a router that +// happens to be between control channels, which is routine rather than a failure to start. Asking whether +// one was ever reached answers the question the check is actually for. +func TestEverConnected_StaysTrueAcrossLosingEveryController(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + ch := &ctrlsTestChannel{id: "ctrl1", label: "first"} + ctrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: ch}, ch, underlay) + require.NoError(t, err) + + // What the dial and accept paths call once the whole bind has succeeded. + nc.MarkChannelEstablished() + require.True(t, nc.EverConnected()) + + // Lose it, leaving no controllers at all, which is what the instant-count check used to trip on. + nc.handleChannelClose("ctrl1", ctrl, 0) + require.Empty(t, nc.GetAll(), "no controllers should remain") + require.True(t, nc.EverConnected(), + "having reached a controller once must not be forgotten when the connection is lost") +} + +// awaitCtrlEvents collects n controller events, failing if they do not arrive in time. +// Collection happens on the calling goroutine: testify runs Eventually and Never conditions +// on their own goroutine, so accumulating into a slice the test body also reads would race. +func awaitCtrlEvents(t *testing.T, drain func() []CtrlEvent, n int) []CtrlEvent { + t.Helper() + + var events []CtrlEvent + deadline := time.Now().Add(2 * time.Second) + for len(events) < n && time.Now().Before(deadline) { + events = append(events, drain()...) + if len(events) < n { + time.Sleep(5 * time.Millisecond) + } + } + + require.GreaterOrEqual(t, len(events), n, "expected %d controller events, got %v", n, events) + return events +} + +// TestNotifyOfConnectivityChange_ReconnectReportsBothHalves guards the reconnect half of a +// grouped control channel losing every underlay and getting one back. The registration +// survives that, so nothing else reports it: a reconnect that re-offers state without +// emitting ControllerReconnected leaves every listener keyed on connectivity believing the +// controller is still gone. +func TestNotifyOfConnectivityChange_ReconnectReportsBothHalves(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + ctrl, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", ctrl) + + var wasDisconnected atomic.Bool + var reconnectNotifications int + notifyReconnect := func() { reconnectNotifications++ } + + // Last underlay lost, then one back. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 0, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 1, notifyReconnect) + + events := awaitCtrlEvents(t, drain, 2) + + // Listeners are notified on their own goroutine per event, so the two can arrive in + // either order; what matters is that both were reported, once each. + require.Len(t, events, 2) + types := []CtrlEventType{events[0].Type, events[1].Type} + require.ElementsMatch(t, []CtrlEventType{ControllerDisconnected, ControllerReconnected}, types, + "regaining an underlay must report the controller as reconnected") + for _, e := range events { + require.Same(t, ctrl, e.Controller) + } + require.Equal(t, 1, reconnectNotifications, "the reconnect must also re-offer state to the controller") +} + +// TestNotifyOfConnectivityChange_ReportsEachEdgeOnce covers underlays coming and going +// without connectivity changing. Only the edges are transitions; every other change is +// noise that listeners must not see as a reconnect. +func TestNotifyOfConnectivityChange_ReportsEachEdgeOnce(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + ctrl, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", ctrl) + + var wasDisconnected atomic.Bool + var reconnectNotifications int + notifyReconnect := func() { reconnectNotifications++ } + + // A second underlay arriving and leaving while one remains is not a transition. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 2, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 1, notifyReconnect) + + require.Never(t, func() bool { return len(drain()) > 0 }, 100*time.Millisecond, 10*time.Millisecond, + "underlay churn that never cost connectivity must not be reported") + require.Zero(t, reconnectNotifications) + + // Two reports of the count reaching zero are one disconnect. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 0, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 0, notifyReconnect) + + events := awaitCtrlEvents(t, drain, 1) + + // Same for the count coming back. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 1, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 2, notifyReconnect) + + events = append(events, awaitCtrlEvents(t, drain, 1)...) + + // Waiting for each edge before triggering the next fixes their order here, unlike the + // back-to-back case, where the per-event goroutines can deliver in either order. + require.Equal(t, ControllerDisconnected, events[0].Type) + require.Equal(t, ControllerReconnected, events[1].Type) + + require.Never(t, func() bool { return len(drain()) > 0 }, 100*time.Millisecond, 10*time.Millisecond, + "each edge must be reported once") + require.Equal(t, 1, reconnectNotifications) +} diff --git a/router/handler_link/bind.go b/router/handler_link/bind.go index 73da7e456..214fbbda5 100644 --- a/router/handler_link/bind.go +++ b/router/handler_link/bind.go @@ -1,6 +1,7 @@ package handler_link import ( + "crypto/x509" "time" "github.com/michaelquigley/pfxlog" @@ -110,14 +111,18 @@ func (self *bindHandler) BindChannel(binding channel.Binding) error { } func (self *bindHandler) verifyRouter(l xlink.Xlink, ch channel.Channel) error { - var fingerprints []string - for _, cert := range ch.Certificates() { - fingerprints = append(fingerprints, nfpem.FingerprintFromCertificate(cert)) + // Fingerprint only the leaf certificate whose key the TLS handshake proved possession of + // (certs[0]). Fingerprinting the rest of the presented chain would let a peer present a victim + // router's certificate as filler and be admitted under the victim's id, since the controller + // accepts the link if any presented fingerprint matches the claimed router's enrolled one. + fingerprint, err := leafFingerprint(ch.Certificates()) + if err != nil { + return errors.Wrapf(err, "unable to verify router for link %v", l.Id()) } verifyLink := &ctrl_pb.VerifyRouter{ RouterId: l.DestinationId(), - Fingerprints: fingerprints, + Fingerprints: []string{fingerprint}, } ctrlCh := self.ctrl.AnyChannel() @@ -145,6 +150,17 @@ func (self *bindHandler) verifyRouter(l xlink.Xlink, ch channel.Channel) error { return errors.Errorf("unable to verify link [%v]", result.Message) } +// leafFingerprint returns the fingerprint of the leaf certificate whose key the TLS handshake proved +// possession of (certs[0]). Only the leaf is fingerprinted: fingerprinting the rest of the presented +// chain would let a peer present a victim router's certificate as filler and be admitted under the +// victim's id. +func leafFingerprint(certs []*x509.Certificate) (string, error) { + if len(certs) == 0 { + return "", errors.New("no certificates presented") + } + return nfpem.FingerprintFromCertificate(certs[0]), nil +} + type heartbeatCallback struct { latencyMetric metrics.Histogram queueTimeMetric metrics.Histogram diff --git a/router/handler_link/bind_test.go b/router/handler_link/bind_test.go new file mode 100644 index 000000000..af9aca718 --- /dev/null +++ b/router/handler_link/bind_test.go @@ -0,0 +1,63 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package handler_link + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" + + nfpem "github.com/openziti/foundation/v2/pem" + "github.com/stretchr/testify/require" +) + +// Test_leafFingerprint covers the fingerprint used to verify an incoming link's router: it must come +// only from the leaf certificate the handshake proved (certs[0]). The central case is that a victim +// router's certificate presented as filler elsewhere in the chain must not contribute a fingerprint, +// so it cannot be matched against the victim's enrolled fingerprint to impersonate the victim. +func Test_leafFingerprint(t *testing.T) { + cert := func(cn string) *x509.Certificate { + return &x509.Certificate{Subject: pkix.Name{CommonName: cn}, Raw: []byte("der-" + cn)} + } + + t.Run("returns the leaf fingerprint", func(t *testing.T) { + req := require.New(t) + leaf := cert("router-A") + + fingerprint, err := leafFingerprint([]*x509.Certificate{leaf}) + req.NoError(err) + req.Equal(nfpem.FingerprintFromCertificate(leaf), fingerprint) + }) + + t.Run("ignores filler certs in the rest of the chain", func(t *testing.T) { + req := require.New(t) + leaf := cert("router-A") + filler := cert("router-D") + + fingerprint, err := leafFingerprint([]*x509.Certificate{leaf, filler}) + req.NoError(err) + req.Equal(nfpem.FingerprintFromCertificate(leaf), fingerprint) + req.NotEqual(nfpem.FingerprintFromCertificate(filler), fingerprint, + "a victim cert presented as filler must not contribute a fingerprint") + }) + + t.Run("rejects when no certificates are presented", func(t *testing.T) { + req := require.New(t) + _, err := leafFingerprint(nil) + req.Error(err) + }) +} diff --git a/router/link/link_registry.go b/router/link/link_registry.go index 31036ab9b..7461413f3 100644 --- a/router/link/link_registry.go +++ b/router/link/link_registry.go @@ -50,14 +50,16 @@ type Env interface { func NewLinkRegistry(routerEnv Env) xlink.Registry { result := &linkRegistryImpl{ - linkMap: map[string]xlink.Xlink{}, - linkByIdMap: map[string]xlink.Xlink{}, - ctrls: routerEnv.GetNetworkControllers(), - events: make(chan event, 16), - env: routerEnv, - destinations: map[string]*linkDest{}, - linkStateQueue: &linkStateHeap{}, - triggerNotifyC: make(chan struct{}, 1), + linkMap: map[string]xlink.Xlink{}, + linkByIdMap: map[string]xlink.Xlink{}, + ctrls: routerEnv.GetNetworkControllers(), + events: make(chan event, 16), + env: routerEnv, + destinations: map[string]*linkDest{}, + linkStateQueue: &linkStateHeap{}, + triggerNotifyC: make(chan struct{}, 1), + fullRefreshSendTimeout: fullRefreshSendTimeout, + fullRefreshRetryDelay: fullRefreshRetryDelay, } go result.run() @@ -73,12 +75,17 @@ type linkRegistryImpl struct { sync.Mutex ctrls env.NetworkControllers - env Env - destinations map[string]*linkDest - linkStateQueue *linkStateHeap - events chan event - triggerNotifyC chan struct{} - notifyInProgress atomic.Bool + env Env + destinations map[string]*linkDest + // fullRefreshSendTimeout and fullRefreshRetryDelay bound and pace the reconnect announcement. Fields so + // tests can reach the timeout, which is the only way to tell a message that was queued and then discarded + // from one that reached the wire. + fullRefreshSendTimeout time.Duration + fullRefreshRetryDelay time.Duration + linkStateQueue *linkStateHeap + events chan event + triggerNotifyC chan struct{} + notifyInProgress atomic.Bool } func (self *linkRegistryImpl) runGcLinkMetricsLoop() { @@ -388,7 +395,49 @@ func (self *linkRegistryImpl) Iter() <-chan xlink.Xlink { return result } +const ( + // fullRefreshSendTimeout bounds one attempt at the reconnect announcement. The registry lock is held for + // the attempt, so this is also how long link accept and dial-succeeded can stall. + fullRefreshSendTimeout = 5 * time.Second + + // fullRefreshSendAttempts bounds the retries. A router announces its full link set once per reconnect and + // nothing re-asks, so an announcement that never lands leaves the controller unable to route over those + // links and unable to prune the ones it should have dropped. + fullRefreshSendAttempts = 3 + + fullRefreshRetryDelay = time.Second +) + func (self *linkRegistryImpl) NotifyOfReconnect(ch channel.Channel) { + // Retried here rather than by clearing the announced marks and letting the periodic path pick it up: that + // path sends without FullRefresh, so the controller cannot prune, and it sends nothing at all when there + // are no established links, which is exactly when stale controller state most needs clearing. + // + // Called on its own goroutine (Router.NotifyOfReconnect), so waiting between attempts blocks nothing. + for attempt := 1; attempt <= fullRefreshSendAttempts; attempt++ { + if self.sendFullRefresh(ch) { + return + } + + if ch.IsClosed() { + return // whatever replaces this channel announces again + } + + if attempt < fullRefreshSendAttempts { + select { + case <-time.After(self.fullRefreshRetryDelay): + case <-self.env.GetCloseNotify(): + return + } + } + } + + pfxlog.Logger().WithField("ctrlId", ch.Id()). + Error("gave up announcing link states after reconnect; this controller cannot route over or prune this router's links until it reconnects") +} + +// sendFullRefresh announces every dialed link to one controller, reporting whether the announcement got out. +func (self *linkRegistryImpl) sendFullRefresh(ch channel.Channel) bool { self.Lock() defer self.Unlock() @@ -420,13 +469,19 @@ func (self *linkRegistryImpl) NotifyOfReconnect(ch channel.Channel) { } } - if err := protobufs.MarshalTyped(routerLinks).Send(ch); err != nil { - logrus.WithError(err).Error("failed to send router links on reconnect") - } else { - for _, f := range onComplete { - f() - } + // SendAndWaitForWire, not Send: Send returns once the message is queued, and the deadline stays live, so + // the tx loop discards it if the queue drains too slowly. Nothing is told, since a plain send's listener + // ignores the error, and this would report success and mark the links synchronized for an announcement + // that never left. Matches how the periodic path sends. + if err := protobufs.MarshalTyped(routerLinks).WithTimeout(self.fullRefreshSendTimeout).SendAndWaitForWire(ch); err != nil { + logrus.WithError(err).WithField("ctrlId", ch.Id()).Error("failed to send router links on reconnect") + return false } + + for _, f := range onComplete { + f() + } + return true } func (self *linkRegistryImpl) GetTraceDecoders() []channel.TraceMessageDecoder { diff --git a/router/link/link_registry_test.go b/router/link/link_registry_test.go index 291b1d6de..c5477e132 100644 --- a/router/link/link_registry_test.go +++ b/router/link/link_registry_test.go @@ -17,6 +17,8 @@ package link import ( + "math" + "sync/atomic" "testing" "time" @@ -31,6 +33,7 @@ import ( "github.com/openziti/ziti/v2/controller/idgen" "github.com/openziti/ziti/v2/router/env" "github.com/openziti/ziti/v2/router/xlink" + "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -356,3 +359,125 @@ func Test_gcLinkMetrics(t *testing.T) { checkLinkMetricsDoesntHave(linkId2, getRegistryMetrics()) checkLinkMetricsDoesntHave(linkId5, getRegistryMetrics()) } + +// flakySendChannel is a channel.Channel double whose sends fail a set number of times before succeeding. It +// embeds the interface, so any method these tests do not exercise panics rather than returning a zero value. +type flakySendChannel struct { + channel.Channel + failures int + sends atomic.Int32 + closed atomic.Bool + closeNotify chan struct{} +} + +func newFlakySendChannel(failures int) *flakySendChannel { + return &flakySendChannel{failures: failures, closeNotify: make(chan struct{})} +} + +func (self *flakySendChannel) Id() string { return "ctrl1" } +func (self *flakySendChannel) Label() string { return "ctrl1" } +func (self *flakySendChannel) IsClosed() bool { return self.closed.Load() } + +func (self *flakySendChannel) CloseNotify() <-chan struct{} { return self.closeNotify } + +func (self *flakySendChannel) Send(s channel.Sendable) error { + if int(self.sends.Add(1)) <= self.failures { + return errors.New("timeout waiting for space in send queue") + } + // What the tx loop does once the message is actually written. + s.SendListener().NotifyAfterWrite() + return nil +} + +// newReconnectTestRegistry builds a registry with no links and no event loop. An empty link set is a case +// worth covering rather than avoiding: the reconnect announcement is the only message that prunes, so a +// router with nothing to report still has to send one to clear stale controller state. +func newReconnectTestRegistry(t *testing.T) (*linkRegistryImpl, *testEnv) { + t.Helper() + routerEnv := newTestEnv() + t.Cleanup(func() { close(routerEnv.closeNotify) }) + + return &linkRegistryImpl{ + env: routerEnv, + ctrls: routerEnv.ctrls, + destinations: map[string]*linkDest{}, + linkMap: map[string]xlink.Xlink{}, + triggerNotifyC: make(chan struct{}, 1), + // Production's pacing is not this test's concern; the timeout has to stay reachable, though, since it + // is what distinguishes a discarded message from a delivered one. + fullRefreshSendTimeout: 20 * time.Millisecond, + fullRefreshRetryDelay: 0, + }, routerEnv +} + +// Test_NotifyOfReconnect_RetriesUntilTheAnnouncementLands covers the retry. A router announces its full link +// set once per controller reconnect and nothing re-asks, so an announcement lost to send-queue back-pressure +// leaves that controller unable to route over the router's links, and unable to prune the ones it should have +// dropped, until the next reconnect. +func Test_NotifyOfReconnect_RetriesUntilTheAnnouncementLands(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := newFlakySendChannel(2) + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(3), ch.sends.Load(), + "the announcement should have been retried until it reached the wire") +} + +// Test_NotifyOfReconnect_GivesUpAfterItsAttempts: the retry is bounded, so a channel that never drains does +// not hold a goroutine indefinitely. +func Test_NotifyOfReconnect_GivesUpAfterItsAttempts(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := newFlakySendChannel(math.MaxInt32) + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(fullRefreshSendAttempts), ch.sends.Load(), + "the retry must stop after its attempts rather than looping") +} + +// Test_NotifyOfReconnect_StopsWhenTheChannelCloses: a closed channel cannot be re-announced to, and whatever +// replaces it announces again, so retrying against it only delays the goroutine's exit. +func Test_NotifyOfReconnect_StopsWhenTheChannelCloses(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := newFlakySendChannel(math.MaxInt32) + ch.closed.Store(true) + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(1), ch.sends.Load(), + "a closed channel should be abandoned after the first failure, not retried") +} + +// acceptThenDiscardChannel models what a real channel does to a message queued behind a backlog: the send is +// accepted, and the message is dropped when its deadline expires before the queue drains. Nothing calls back, +// because a plain send's listener ignores the error. +type acceptThenDiscardChannel struct { + channel.Channel + sends atomic.Int32 + closeNotify chan struct{} +} + +func (self *acceptThenDiscardChannel) Id() string { return "ctrl1" } +func (self *acceptThenDiscardChannel) Label() string { return "ctrl1" } +func (self *acceptThenDiscardChannel) IsClosed() bool { return false } +func (self *acceptThenDiscardChannel) CloseNotify() <-chan struct{} { return self.closeNotify } + +func (self *acceptThenDiscardChannel) Send(channel.Sendable) error { + self.sends.Add(1) + return nil // queued, and never written +} + +// Test_NotifyOfReconnect_TreatsADiscardedAnnouncementAsFailure is the guard on how the announcement is sent. +// A plain Send returns once the message is queued, and the deadline stays live afterwards, so the tx loop can +// drop it with nobody told: a plain send's listener ignores the error. Reporting that as success marks the +// links announced and leaves the controller permanently without them. +func Test_NotifyOfReconnect_TreatsADiscardedAnnouncementAsFailure(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := &acceptThenDiscardChannel{closeNotify: make(chan struct{})} + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(fullRefreshSendAttempts), ch.sends.Load(), + "an announcement accepted into the queue but never written must count as a failure and be retried") +} diff --git a/router/posture/errors.go b/router/posture/errors.go index 2e3757154..0cf02be07 100644 --- a/router/posture/errors.go +++ b/router/posture/errors.go @@ -2,7 +2,9 @@ package posture import ( "fmt" + "runtime/debug" + "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" "github.com/pkg/errors" ) @@ -66,7 +68,24 @@ func (pae *PolicyAccessErrors) Error() string { return fmt.Sprintf("%d policies failed: %s", len(*pae), subErr) } -func EvaluatePostureCheck(postureCheck *edge_ctrl_pb.DataState_PostureCheck, data *InstanceData) *CheckError { +// EvaluatePostureCheck evaluates a single posture check against the supplied posture +// state. It never panics: an evaluation panic is recovered and reported as a failed +// check, denying access instead of crashing the router. +func EvaluatePostureCheck(postureCheck *edge_ctrl_pb.DataState_PostureCheck, data *InstanceData) (result *CheckError) { + defer func() { + if r := recover(); r != nil { + pfxlog.Logger().WithField("postureCheckId", postureCheck.Id). + WithField("postureCheckName", postureCheck.Name). + Errorf("posture check evaluation panicked, treating as failed: %v\n%s", r, debug.Stack()) + + result = &CheckError{ + Id: postureCheck.Id, + Name: postureCheck.Name, + Cause: fmt.Errorf("posture check evaluation panicked: %v", r), + } + } + }() + check := CtrlCheckToLogic(postureCheck) return check.Evaluate(data) } diff --git a/router/posture/mfa.go b/router/posture/mfa.go index 59a711a14..9948a2b89 100644 --- a/router/posture/mfa.go +++ b/router/posture/mfa.go @@ -50,11 +50,11 @@ func (m *MfaCheck) Evaluate(state *InstanceData) *CheckError { } } - if m.PromptOnWake { + if m.PromptOnWake && state.Woken != nil { wokenAt := state.Woken.Time.AsTime() wokenGraceEndsAt := wokenAt.Add(PromptGracePeriod) - if now.After(wokenGraceEndsAt) { + if wokenAt.After(*state.PassedMfaAt) && now.After(wokenGraceEndsAt) { return &CheckError{ Id: m.Id, Name: m.Name, @@ -63,11 +63,11 @@ func (m *MfaCheck) Evaluate(state *InstanceData) *CheckError { } } - if m.PromptOnUnlock { + if m.PromptOnUnlock && state.Unlocked != nil { unlockedAt := state.Unlocked.Time.AsTime() unlockedGraceEndsAt := unlockedAt.Add(PromptGracePeriod) - if now.After(unlockedGraceEndsAt) { + if unlockedAt.After(*state.PassedMfaAt) && now.After(unlockedGraceEndsAt) { return &CheckError{ Id: m.Id, Name: m.Name, diff --git a/router/posture/mfa_test.go b/router/posture/mfa_test.go new file mode 100644 index 000000000..6f2f56839 --- /dev/null +++ b/router/posture/mfa_test.go @@ -0,0 +1,158 @@ +package posture + +import ( + "testing" + "time" + + "github.com/openziti/sdk-golang/pb/edge_client_pb" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func newMfaCheck(promptOnWake, promptOnUnlock bool) *MfaCheck { + return &MfaCheck{ + DataState_PostureCheck: &edge_ctrl_pb.DataState_PostureCheck{ + Id: "test-mfa-id", + Name: "my-mfa-check", + }, + DataState_PostureCheck_Mfa: &edge_ctrl_pb.DataState_PostureCheck_Mfa{ + TimeoutSeconds: NoTimeout, + PromptOnWake: promptOnWake, + PromptOnUnlock: promptOnUnlock, + }, + } +} + +func newMfaInstanceData(passedMfaAt time.Time) *InstanceData { + return &InstanceData{ + IdentityId: "test-identity", + ApiSessionId: "test-api-session", + PassedMfaAt: &passedMfaAt, + } +} + +// MFA passed, promptOnWake enabled, but no wake event has ever been reported: Woken is nil. +// Must pass without panicking, matching the controller's PassedOnWake semantics. +func TestMfaCheck_PromptOnWake_NilWoken_Passes(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when promptOnWake is set and no wake event was reported, got: %v", result) + } +} + +// MFA passed, promptOnUnlock enabled, but no unlock event has ever been reported: Unlocked is nil. +// Must pass without panicking, matching the controller's PassedOnUnlock semantics. +func TestMfaCheck_PromptOnUnlock_NilUnlocked_Passes(t *testing.T) { + check := newMfaCheck(false, true) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when promptOnUnlock is set and no unlock event was reported, got: %v", result) + } +} + +// Both prompts enabled, neither event reported. Must pass without panicking. +func TestMfaCheck_BothPrompts_NilEvents_Passes(t *testing.T) { + check := newMfaCheck(true, true) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when no wake/unlock events were reported, got: %v", result) + } +} + +// A wake event that occurred before MFA was last passed is satisfied by that MFA pass. +func TestMfaCheck_PromptOnWake_WokenBeforeMfa_Passes(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + data.Woken = &edge_client_pb.PostureResponse_Woken{ + Time: timestamppb.New(time.Now().Add(-time.Hour)), + } + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when wake event predates the MFA pass, got: %v", result) + } +} + +// An unlock event that occurred before MFA was last passed is satisfied by that MFA pass. +func TestMfaCheck_PromptOnUnlock_UnlockedBeforeMfa_Passes(t *testing.T) { + check := newMfaCheck(false, true) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + data.Unlocked = &edge_client_pb.PostureResponse_Unlocked{ + Time: timestamppb.New(time.Now().Add(-time.Hour)), + } + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when unlock event predates the MFA pass, got: %v", result) + } +} + +// A wake event after the last MFA pass, still within the grace period, passes. +func TestMfaCheck_PromptOnWake_WokenAfterMfaWithinGrace_Passes(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Hour)) + data.Woken = &edge_client_pb.PostureResponse_Woken{ + Time: timestamppb.New(time.Now().Add(-time.Minute)), + } + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass for wake event within the grace period, got: %v", result) + } +} + +// A wake event after the last MFA pass, beyond the grace period, fails. +func TestMfaCheck_PromptOnWake_WokenAfterMfaBeyondGrace_Fails(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Hour)) + data.Woken = &edge_client_pb.PostureResponse_Woken{ + Time: timestamppb.New(time.Now().Add(-10 * time.Minute)), + } + + if result := check.Evaluate(data); result == nil { + t.Fatal("expected failure for wake event beyond the grace period without MFA resupply, got pass") + } +} + +// An unlock event after the last MFA pass, beyond the grace period, fails. +func TestMfaCheck_PromptOnUnlock_UnlockedAfterMfaBeyondGrace_Fails(t *testing.T) { + check := newMfaCheck(false, true) + data := newMfaInstanceData(time.Now().Add(-time.Hour)) + data.Unlocked = &edge_client_pb.PostureResponse_Unlocked{ + Time: timestamppb.New(time.Now().Add(-10 * time.Minute)), + } + + if result := check.Evaluate(data); result == nil { + t.Fatal("expected failure for unlock event beyond the grace period without MFA resupply, got pass") + } +} + +// MFA never passed still fails regardless of prompt flags. +func TestMfaCheck_NeverPassedMfa_Fails(t *testing.T) { + check := newMfaCheck(true, true) + data := &InstanceData{ + IdentityId: "test-identity", + ApiSessionId: "test-api-session", + } + + if result := check.Evaluate(data); result == nil { + t.Fatal("expected failure when MFA has never been passed, got pass") + } +} + +// EvaluatePostureCheck must convert an evaluation panic into a failed check, never +// propagate it to the caller. An unknown subtype produces a nil Checker whose +// Evaluate call panics. +func TestEvaluatePostureCheck_PanicBecomesFailedCheck(t *testing.T) { + postureCheck := &edge_ctrl_pb.DataState_PostureCheck{ + Id: "test-unknown-id", + Name: "my-unknown-check", + } + + result := EvaluatePostureCheck(postureCheck, &InstanceData{}) + + if result == nil { + t.Fatal("expected a non-nil CheckError for an unevaluable posture check, got nil") + } +} diff --git a/router/router.go b/router/router.go index f729d6910..3f58e5699 100644 --- a/router/router.go +++ b/router/router.go @@ -886,12 +886,26 @@ func (self *Router) startControlPlane() error { if self.config.Ctrl.StartupTimeout > 0 { time.AfterFunc(self.config.Ctrl.StartupTimeout, func() { - if !self.isShutdown.Load() && len(self.ctrls.GetAll()) == 0 { - if os.Getenv("STACKDUMP_ON_FAILED_STARTUP") == "true" { - debugz.DumpStack() - } - pfxlog.Logger().Fatal("unable to connect to any controllers before timeout") + if self.isShutdown.Load() { + return } + + log := pfxlog.Logger(). + WithField("startupTimeout", self.config.Ctrl.StartupTimeout). + WithField("everConnected", self.ctrls.EverConnected()). + WithField("connectedCount", len(self.ctrls.GetAll())) + + // This fires once, so sampling the current count kills a router that happens to be between + // control channels when it lands, which is routine rather than a failure to start. + if self.ctrls.EverConnected() { + log.Info("startup connection check passed") + return + } + + if os.Getenv("STACKDUMP_ON_FAILED_STARTUP") == "true" { + debugz.DumpStack() + } + log.Fatal("unable to connect to any controllers before timeout") }) } diff --git a/router/router_test.go b/router/router_test.go index f351ac2e0..600e0dedc 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -51,6 +51,9 @@ func (m *mockNetworkControllers) UpdateControllerDetails(controllers []*ctrl_pb. return changed } +func (m *mockNetworkControllers) MarkChannelEstablished() {} +func (m *mockNetworkControllers) EverConnected() bool { return true } + func (m *mockNetworkControllers) ConnectToInitialEndpoints(endpoints []string) { for _, ep := range endpoints { m.endpoints[ep] = struct{}{} diff --git a/router/state/cert_origin.go b/router/state/cert_origin.go index 52f64044c..f852c4b94 100644 --- a/router/state/cert_origin.go +++ b/router/state/cert_origin.go @@ -23,6 +23,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" ) // CertOrigin indicates whether a client certificate was issued by the internal (first-party) CA @@ -35,23 +36,60 @@ const ( CertOriginThirdParty ) -// controllerRootCache caches the root CA extracted from a controller's cert chain. +// controllerRootCache caches the root CAs extracted from a controller's cert chain. type controllerRootCache struct { - mu sync.RWMutex - rootPool *x509.CertPool - inited bool + mu sync.RWMutex + roots []*x509.Certificate + inited bool } -// IsFirstPartyCert reports whether the leaf of peerCerts chains to a controller-trusted -// root CA. peerCerts is a TLS peer chain with the leaf at index 0; remaining entries are -// used as intermediates. Time validity is not checked; callers enforce expiry. +// IsFirstPartyCert reports whether the leaf of peerCerts chains to a first-party trust +// anchor: a router data model public key with the FirstPartyX509CertValidation usage, or +// a root CA from the ctrl channel certificate chain (covers controllers that predate the +// first-party usage). peerCerts is a TLS peer chain with the leaf at index 0; remaining +// entries are used as intermediates, along with intermediates published on the data model +// keys. Time validity is not checked; callers enforce expiry. func (self *ManagerImpl) IsFirstPartyCert(peerCerts []*x509.Certificate) bool { if len(peerCerts) == 0 { return false } - pool := self.getControllerRootPool() - if pool == nil { + roots := x509.NewCertPool() + intermediates := x509.NewCertPool() + rootCount := 0 + + if rdm := self.routerDataModel.Load(); rdm != nil { + for keysTuple := range rdm.PublicKeys.IterBuffered() { + publicKey := keysTuple.Val + if !contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) { + continue + } + + anchor, err := self.getX509FromData(publicKey.Kid, publicKey.GetData()) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse x509 certificate data for first party public key") + continue + } + roots.AddCert(anchor) + rootCount++ + + for _, intermediateDer := range publicKey.Intermediates { + intermediate, err := x509.ParseCertificate(intermediateDer) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse intermediate certificate data for first party public key") + continue + } + intermediates.AddCert(intermediate) + } + } + } + + for _, root := range self.getControllerRoots() { + roots.AddCert(root) + rootCount++ + } + + if rootCount == 0 { return false } @@ -61,13 +99,12 @@ func (self *ManagerImpl) IsFirstPartyCert(peerCerts []*x509.Certificate) bool { certCopy.NotBefore = time.Now().Add(-1 * time.Hour) certCopy.NotAfter = time.Now().Add(1 * time.Hour) - intermediates := x509.NewCertPool() for _, c := range peerCerts[1:] { intermediates.AddCert(c) } opts := x509.VerifyOptions{ - Roots: pool, + Roots: roots, Intermediates: intermediates, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, } @@ -79,12 +116,12 @@ func (self *ManagerImpl) IsFirstPartyCert(peerCerts []*x509.Certificate) bool { return false } -func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { +func (self *ManagerImpl) getControllerRoots() []*x509.Certificate { self.ctrlRootCache.mu.RLock() if self.ctrlRootCache.inited { - pool := self.ctrlRootCache.rootPool + roots := self.ctrlRootCache.roots self.ctrlRootCache.mu.RUnlock() - return pool + return roots } self.ctrlRootCache.mu.RUnlock() @@ -93,7 +130,7 @@ func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { // double-check after acquiring write lock if self.ctrlRootCache.inited { - return self.ctrlRootCache.rootPool + return self.ctrlRootCache.roots } ctrls := self.env.GetNetworkControllers() @@ -110,10 +147,10 @@ func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { } // Walk the cert chain to find the root (self-signed) CA. - rootPool := x509.NewCertPool() + var roots []*x509.Certificate for _, cert := range certs { if identity.IsRootCa(cert) { - rootPool.AddCert(cert) + roots = append(roots, cert) } } @@ -128,14 +165,13 @@ func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { } if chains, err := certs[0].Verify(opts); err == nil { for _, chain := range chains { - root := chain[len(chain)-1] - rootPool.AddCert(root) + roots = append(roots, chain[len(chain)-1]) } } } } self.ctrlRootCache.inited = true - self.ctrlRootCache.rootPool = rootPool - return rootPool + self.ctrlRootCache.roots = roots + return roots } diff --git a/router/state/cert_origin_test.go b/router/state/cert_origin_test.go new file mode 100644 index 000000000..f5a2f8ae1 --- /dev/null +++ b/router/state/cert_origin_test.go @@ -0,0 +1,381 @@ +package state + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "math/big" + "testing" + "time" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" + + "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" +) + +// testCa holds a CA certificate and its signing key for issuing test certificates. +type testCa struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +// newTestRootCa creates a self-signed root CA. +func newTestRootCa(name string) (*testCa, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, big.NewInt(int64(1)<<62)) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return &testCa{cert: cert, key: key}, nil +} + +// newIntermediateCa creates an intermediate CA issued by this CA. +func (ca *testCa) newIntermediateCa(name string) (*testCa, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, big.NewInt(int64(1)<<62)) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return &testCa{cert: cert, key: key}, nil +} + +// issueClientCert issues a client leaf certificate from this CA. +func (ca *testCa) issueClientCert(name string) (*x509.Certificate, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, big.NewInt(int64(1)<<62)) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key) + if err != nil { + return nil, err + } + + return x509.ParseCertificate(der) +} + +// newCertOriginTestManager builds a ManagerImpl with just enough state for +// IsFirstPartyCert: a pre-seeded ctrl-channel root pool and an empty router data model. +func newCertOriginTestManager(ctrlRoot *x509.Certificate) *ManagerImpl { + mgr := &ManagerImpl{ + certCache: cmap.New[*x509.Certificate](), + } + + if ctrlRoot != nil { + mgr.ctrlRootCache.roots = []*x509.Certificate{ctrlRoot} + } + mgr.ctrlRootCache.inited = true + + mgr.routerDataModel.Store(common.NewBareRouterDataModel()) + + return mgr +} + +// publishPublicKey adds a public key to the manager's router data model with the given +// usages and optional published intermediates. +func publishPublicKey(mgr *ManagerImpl, kid string, cert *x509.Certificate, usages []edge_ctrl_pb.DataState_PublicKey_Usage, intermediates ...*x509.Certificate) { + publicKey := &edge_ctrl_pb.DataState_PublicKey{ + Data: cert.Raw, + Kid: kid, + Usages: usages, + Format: edge_ctrl_pb.DataState_PublicKey_X509CertDer, + } + + for _, intermediate := range intermediates { + publicKey.Intermediates = append(publicKey.Intermediates, intermediate.Raw) + } + + mgr.routerDataModel.Load().PublicKeys.Set(kid, publicKey) +} + +func Test_IsFirstPartyCert(t *testing.T) { + ctrlCa, err := newTestRootCa("ctrl-root") + require.NoError(t, err) + + signingCa, err := newTestRootCa("edge-signing-root") + require.NoError(t, err) + + thirdPartyCa, err := newTestRootCa("third-party-root") + require.NoError(t, err) + + t.Run("cert issued by the ctrl channel root is first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + clientCert, err := ctrlCa.issueClientCert("client-ctrl") + req.NoError(err) + + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by a distinct edge signing CA published as first party is first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + publishPublicKey(mgr, "signing-root", signingCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + }) + + clientCert, err := signingCa.issueClientCert("client-signing") + req.NoError(err) + + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by a published intermediate of the edge signing CA is first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + intermediateCa, err := signingCa.newIntermediateCa("edge-signing-intermediate") + req.NoError(err) + + publishPublicKey(mgr, "signing-root", signingCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + }, intermediateCa.cert) + + clientCert, err := intermediateCa.issueClientCert("client-intermediate") + req.NoError(err) + + // leaf only; the intermediate must come from the published key, not the peer chain + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by a third party CA is not first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + publishPublicKey(mgr, "third-party-root", thirdPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation, + }) + + clientCert, err := thirdPartyCa.issueClientCert("client-third-party") + req.NoError(err) + + req.False(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by an unpublished CA is not first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + clientCert, err := signingCa.issueClientCert("client-unpublished") + req.NoError(err) + + req.False(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("old controller publishing only deprecated usage retains prior behavior", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + publishPublicKey(mgr, "signing-root", signingCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + }) + + ctrlClientCert, err := ctrlCa.issueClientCert("client-ctrl-old") + req.NoError(err) + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{ctrlClientCert})) + + signingClientCert, err := signingCa.issueClientCert("client-signing-old") + req.NoError(err) + req.False(mgr.IsFirstPartyCert([]*x509.Certificate{signingClientCert})) + }) + + t.Run("no peer certs is not first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + req.False(mgr.IsFirstPartyCert(nil)) + }) +} + +// parseCertDirect is a parseCert callback for buildClientCertRoots without caching. +func parseCertDirect(_ string, data []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(data) +} + +// verifyAgainstRdmPools verifies cert against the root and intermediate pools built by +// buildClientCertRoots from rdm. A nil result means the cert chained to an anchor. +func verifyAgainstRdmPools(rdm *common.RouterDataModel, cert *x509.Certificate) error { + roots, published, count := buildClientCertRoots(rdm, parseCertDirect) + if count == 0 { + return errors.New("no anchors selected") + } + intermediates := x509.NewCertPool() + for _, intermediate := range published { + intermediates.AddCert(intermediate) + } + _, err := cert.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + return err +} + +func Test_buildClientCertRoots(t *testing.T) { + firstPartyCa, err := newTestRootCa("first-party-root") + require.NoError(t, err) + + thirdPartyCa, err := newTestRootCa("third-party-root") + require.NoError(t, err) + + t.Run("first and third party keys are both anchors", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "first", firstPartyCa.cert, firstPartyCaUsagesForTest()) + publishPublicKey(mgr, "third", thirdPartyCa.cert, thirdPartyCaUsagesForTest()) + rdm := mgr.routerDataModel.Load() + + firstPartyClient, err := firstPartyCa.issueClientCert("client-first") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, firstPartyClient)) + + thirdPartyClient, err := thirdPartyCa.issueClientCert("client-third") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, thirdPartyClient)) + }) + + t.Run("deprecated-only key is ignored when new usages are present", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "first", firstPartyCa.cert, firstPartyCaUsagesForTest()) + publishPublicKey(mgr, "deprecated", thirdPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + }) + rdm := mgr.routerDataModel.Load() + + thirdPartyClient, err := thirdPartyCa.issueClientCert("client-third") + req.NoError(err) + req.Error(verifyAgainstRdmPools(rdm, thirdPartyClient)) + }) + + t.Run("deprecated keys are anchors when no key has the new usages", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "deprecated", firstPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + }) + rdm := mgr.routerDataModel.Load() + + firstPartyClient, err := firstPartyCa.issueClientCert("client-first") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, firstPartyClient)) + }) + + t.Run("published intermediates chain leaf certs to the anchor", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + + intermediateCa, err := firstPartyCa.newIntermediateCa("first-party-intermediate") + req.NoError(err) + + publishPublicKey(mgr, "first", firstPartyCa.cert, firstPartyCaUsagesForTest(), intermediateCa.cert) + rdm := mgr.routerDataModel.Load() + + leaf, err := intermediateCa.issueClientCert("client-intermediate") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, leaf)) + }) + + t.Run("jwt-only key is not an anchor", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "jwt", firstPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_JWTValidation, + }) + rdm := mgr.routerDataModel.Load() + + firstPartyClient, err := firstPartyCa.issueClientCert("client-first") + req.NoError(err) + req.Error(verifyAgainstRdmPools(rdm, firstPartyClient)) + }) +} + +// firstPartyCaUsagesForTest returns the usage set a current controller publishes for +// first-party CA roots. +func firstPartyCaUsagesForTest() []edge_ctrl_pb.DataState_PublicKey_Usage { + return []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + } +} + +// thirdPartyCaUsagesForTest returns the usage set a current controller publishes for +// third-party CAs. +func thirdPartyCaUsagesForTest() []edge_ctrl_pb.DataState_PublicKey_Usage { + return []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation, + } +} diff --git a/router/state/cert_validating_identity.go b/router/state/cert_validating_identity.go index fea3a9663..77aac7677 100644 --- a/router/state/cert_validating_identity.go +++ b/router/state/cert_validating_identity.go @@ -25,13 +25,14 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" ) // certValidatingIdentity wraps an identity.Identity to add client certificate chain verification // at the TLS level via a VerifyConnection callback. It dynamically builds a CA pool from the -// router data model's PublicKeys with ClientX509CertValidation usage, ensuring the latest -// trust anchors are always used. +// router data model's client cert validation PublicKeys, ensuring the latest trust anchors +// are always used. type certValidatingIdentity struct { identity.Identity stateManager Manager @@ -57,6 +58,54 @@ func (self *certValidatingIdentity) ServerTLSConfig() *tls.Config { return cfg } +// buildClientCertRoots builds the trust anchor pool for client certificate verification +// from the router data model's public keys. Keys with the FirstPartyX509CertValidation or +// ThirdPartyX509CertValidation usage are trust anchors; when no key carries either usage +// (controller predates the first/third-party split), keys with the deprecated +// ClientX509CertValidation usage are used instead. Intermediates published on the selected +// keys are returned separately so callers can add them to their intermediate pool. +// parseCert converts a key's anchor data to a certificate, allowing callers to supply caching. +func buildClientCertRoots(rdm *common.RouterDataModel, parseCert func(kid string, data []byte) (*x509.Certificate, error)) (roots *x509.CertPool, intermediates []*x509.Certificate, rootCount int) { + roots = x509.NewCertPool() + + var anchors []*edge_ctrl_pb.DataState_PublicKey + var fallback []*edge_ctrl_pb.DataState_PublicKey + for keysTuple := range rdm.PublicKeys.IterBuffered() { + publicKey := keysTuple.Val + if contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) || + contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation) { + anchors = append(anchors, publicKey) + } else if contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) { + fallback = append(fallback, publicKey) + } + } + + if len(anchors) == 0 { + anchors = fallback + } + + for _, publicKey := range anchors { + anchor, err := parseCert(publicKey.Kid, publicKey.GetData()) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse x509 certificate data for client cert verification") + continue + } + roots.AddCert(anchor) + rootCount++ + + for _, intermediateDer := range publicKey.Intermediates { + intermediate, err := x509.ParseCertificate(intermediateDer) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse intermediate certificate data for client cert verification") + continue + } + intermediates = append(intermediates, intermediate) + } + } + + return roots, intermediates, rootCount +} + // verifyConnection is a TLS VerifyConnection callback that verifies client certificates against // the CA pool built from RDM PublicKeys. func (self *certValidatingIdentity) verifyConnection(state tls.ConnectionState) error { @@ -71,32 +120,26 @@ func (self *certValidatingIdentity) verifyConnection(state tls.ConnectionState) return errors.New("router data model not yet available, cannot verify client certificate") } - rootPool := x509.NewCertPool() - intermediatePool := x509.NewCertPool() - certCount := 0 - for keysTuple := range rdm.PublicKeys.IterBuffered() { - if contains(keysTuple.Val.Usages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) { - parsed, err := x509.ParseCertificate(keysTuple.Val.GetData()) - if err != nil { - pfxlog.Logger().WithField("kid", keysTuple.Val.Kid).WithError(err).Error("could not parse x509 certificate data for TLS client verification") - continue - } - rootPool.AddCert(parsed) - certCount++ - } - } + rootPool, publishedIntermediates, certCount := buildClientCertRoots(rdm, func(_ string, data []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(data) + }) if certCount == 0 { return errors.New("no trusted CA certificates available in router data model") } - // Add the router identity's CA bundle as intermediates. The RDM PublicKeys typically - // contain only root CAs, but client certs may be signed by an intermediate CA that - // is part of the router's trust bundle. + // Start the intermediate pool from the router identity's CA bundle. The RDM anchors are + // typically root CAs, but client certs may be signed by an intermediate CA that is part + // of the router's trust bundle rather than published on an anchor. + intermediatePool := x509.NewCertPool() if idCa := self.Identity.CA(); idCa != nil { intermediatePool = idCa.Clone() } + for _, intermediate := range publishedIntermediates { + intermediatePool.AddCert(intermediate) + } + // Also add any additional certs from the TLS peer chain as intermediates. for _, cert := range state.PeerCertificates[1:] { intermediatePool.AddCert(cert) diff --git a/router/state/manager.go b/router/state/manager.go index 166a0b6bb..f45637356 100644 --- a/router/state/manager.go +++ b/router/state/manager.go @@ -857,27 +857,18 @@ func (self *ManagerImpl) getX509FromData(kid string, data []byte) (*x509.Certifi // certificate authorities, ensuring only properly signed certificates can // establish authenticated connections. func (self *ManagerImpl) VerifyClientCert(cert *x509.Certificate) error { - - rootPool := x509.NewCertPool() - rdm := self.routerDataModel.Load() - for keysTuple := range rdm.PublicKeys.IterBuffered() { - if contains(keysTuple.Val.Usages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) { - cert, err := self.getX509FromData(keysTuple.Val.Kid, keysTuple.Val.GetData()) + rootPool, publishedIntermediates, _ := buildClientCertRoots(rdm, self.getX509FromData) - if err != nil { - pfxlog.Logger().WithField("kid", keysTuple.Val.Kid).WithError(err).Error("could not parse x509 certificate data") - continue - } - - rootPool.AddCert(cert) - } + intermediatePool := x509.NewCertPool() + for _, intermediate := range publishedIntermediates { + intermediatePool.AddCert(intermediate) } opts := x509.VerifyOptions{ Roots: rootPool, - Intermediates: x509.NewCertPool(), + Intermediates: intermediatePool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, CurrentTime: cert.NotBefore, } diff --git a/router/xgress_common/connection.go b/router/xgress_common/connection.go index b82770864..886f37599 100644 --- a/router/xgress_common/connection.go +++ b/router/xgress_common/connection.go @@ -253,6 +253,15 @@ func (self *XgressConn) ReadPayload() ([]byte, map[uint8][]byte, error) { // next time return EOF return nil, nil, io.EOF } + + // This conn has no half-close, so a read EOF means the local side is finished for + // good and will never signal anything further. Report a full close rather than a bare + // io.EOF, which Xgress.rx() would treat as a half-close, leaving the xgress waiting on + // an end-of-circuit that cannot arrive. + // + // The liveness probe above cannot catch this: conns without half-close include the + // datagram ones, and udp_vconn implements deadlines as no-ops that always succeed. + return nil, nil, xgress.ErrPeerClosed } return buffer, nil, err diff --git a/router/xgress_common/connection_test.go b/router/xgress_common/connection_test.go new file mode 100644 index 000000000..82099d6e2 --- /dev/null +++ b/router/xgress_common/connection_test.go @@ -0,0 +1,87 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xgress_common + +import ( + "io" + "net" + "testing" + "time" + + "github.com/openziti/sdk-golang/xgress" + "github.com/stretchr/testify/require" +) + +// eofConn is a connection whose reads have run out. Its deadline setters succeed, matching the +// datagram connections that implement them as no-ops, so the liveness probe in ReadPayload +// cannot tell that the peer is gone. +type eofConn struct { + net.Conn +} + +func (self *eofConn) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (self *eofConn) Write(b []byte) (int, error) { + return len(b), nil +} + +func (self *eofConn) Close() error { + return nil +} + +func (self *eofConn) SetReadDeadline(time.Time) error { return nil } +func (self *eofConn) SetWriteDeadline(time.Time) error { return nil } + +// TestReadPayloadEofWithoutHalfClose covers a connection with no half-close semantics reaching +// end of input. There is nothing further the local side can signal, so this has to be reported +// as a full close. Reporting a bare io.EOF instead reads as a half-close to Xgress.rx(), which +// leaves the xgress waiting on an end-of-circuit that will never arrive. +func TestReadPayloadEofWithoutHalfClose(t *testing.T) { + req := require.New(t) + + conn := NewXgressConn(&eofConn{}, false, ConnTypeTunnel) + + data, headers, err := conn.ReadPayload() + req.ErrorIs(err, xgress.ErrPeerClosed) + req.Nil(data) + req.Nil(headers) + + // still reported on a subsequent read, rather than reverting to a half-close + _, _, err = conn.ReadPayload() + req.ErrorIs(err, xgress.ErrPeerClosed) +} + +// TestReadPayloadEofWithHalfClose covers the half-close path, which must keep reporting the FIN +// headers followed by io.EOF so the write half can close while the read half stays open. +func TestReadPayloadEofWithHalfClose(t *testing.T) { + req := require.New(t) + + conn := NewXgressConn(&eofConn{}, true, ConnTypeTunnel) + + data, headers, err := conn.ReadPayload() + req.NoError(err) + req.Nil(data) + req.Equal(GetFinHeaders(), headers, "first read at end of input should carry the fin headers") + + data, headers, err = conn.ReadPayload() + req.ErrorIs(err, io.EOF) + req.Empty(data) + req.Nil(headers) + req.False(conn.IsClosed(), "half-close should leave the conn open for reading") +} diff --git a/router/xgress_test/xgress_teardown_test.go b/router/xgress_test/xgress_teardown_test.go new file mode 100644 index 000000000..542fdadac --- /dev/null +++ b/router/xgress_test/xgress_teardown_test.go @@ -0,0 +1,131 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xgress_test + +import ( + "io" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/sdk-golang/xgress" + "github.com/openziti/ziti/v2/router/xgress_common" + "github.com/stretchr/testify/require" +) + +// TestXgressClosesWhenPeerEndsWithoutHalfClose covers a circuit whose initiating connection has +// no half-close semantics, as datagram flows from an intercepting tunneler do. When such a +// connection reaches end of input it is finished for good, so the xgress must close and take the +// circuit with it. +// +// Treating that end of input as a half-close instead leaves the xgress waiting on an +// end-of-circuit its peer has no reason to send, so it is never closed, never reported to the +// controller, and holds its goroutines and buffers until the process exits. +func TestXgressClosesWhenPeerEndsWithoutHalfClose(t *testing.T) { + req := require.New(t) + + adapter := NewMockDataPlaneAdapter() + defer adapter.Close() + + circuitId := "teardown-circuit" + initAddr := xgress.Address("init") + termAddr := xgress.Address("term") + + options := xgress.DefaultOptions() + + // halfClose false: the flow has no way to signal a write-side close on its own + endedFlow := &endedFlowConn{} + initiatorConn := xgress_common.NewXgressConn(endedFlow, false, xgress_common.ConnTypeTunnel) + terminatorConn := &blockingConn{closeNotify: make(chan struct{})} + + initiator := xgress.NewXgress(circuitId, "test", initAddr, initiatorConn, xgress.Initiator, options, nil) + terminator := xgress.NewXgress(circuitId, "test", termAddr, terminatorConn, xgress.Terminator, options, nil) + + initiator.SetDataPlaneAdapter(adapter) + terminator.SetDataPlaneAdapter(adapter) + adapter.RegisterXgress(initiator) + adapter.RegisterXgress(terminator) + adapter.ConnectCircuit(circuitId, initAddr, termAddr) + adapter.ConnectCircuit(circuitId, termAddr, initAddr) + + initiatorClosed := make(chan struct{}) + initiator.AddCloseHandler(xgress.CloseHandlerF(func(*xgress.Xgress) { + close(initiatorClosed) + })) + + initiator.Start() + terminator.Start() + + select { + case <-initiatorClosed: + case <-time.After(30 * time.Second): + req.Fail("initiating xgress was never closed after its connection ended") + } + + req.True(initiator.IsClosed(), "initiating xgress should report closed") + req.Equal(int32(1), endedFlow.closeCount.Load(), + "closing the xgress should release the connection underneath it, exactly once") +} + +// blockingConn is a peer that never produces or accepts anything, standing in for a hosted +// application that has no reason to speak first. +type blockingConn struct { + closeNotify chan struct{} + closed bool +} + +func (self *blockingConn) LogContext() string { return "blocking" } + +func (self *blockingConn) ReadPayload() ([]byte, map[uint8][]byte, error) { + <-self.closeNotify + return nil, nil, io.EOF +} + +func (self *blockingConn) WritePayload(b []byte, _ map[uint8][]byte) (int, error) { + return len(b), nil +} + +func (self *blockingConn) Close() error { + if !self.closed { + self.closed = true + close(self.closeNotify) + } + return nil +} + +func (self *blockingConn) HandleControlMsg(xgress.ControlType, channel.Headers, xgress.ControlReceiver) error { + return nil +} + +// endedFlowConn models a datagram flow whose association has expired. Reads report end of input, +// and the deadline setters succeed the way udp_vconn's no-op implementations do, so the liveness +// probe in ReadPayload cannot tell that the flow is gone. +type endedFlowConn struct { + net.Conn + closeCount atomic.Int32 +} + +func (self *endedFlowConn) Read([]byte) (int, error) { return 0, io.EOF } +func (self *endedFlowConn) Write(b []byte) (int, error) { return len(b), nil } +func (self *endedFlowConn) Close() error { self.closeCount.Add(1); return nil } +func (self *endedFlowConn) LocalAddr() net.Addr { return nil } +func (self *endedFlowConn) RemoteAddr() net.Addr { return nil } +func (self *endedFlowConn) SetDeadline(time.Time) error { return nil } +func (self *endedFlowConn) SetReadDeadline(time.Time) error { return nil } +func (self *endedFlowConn) SetWriteDeadline(time.Time) error { return nil } diff --git a/router/xlink_transport/connect.go b/router/xlink_transport/connect.go index ffaabea85..22787abf2 100644 --- a/router/xlink_transport/connect.go +++ b/router/xlink_transport/connect.go @@ -23,6 +23,7 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/cert" log "github.com/sirupsen/logrus" ) @@ -35,14 +36,6 @@ func (self *ConnectionHandler) HandleConnection(hello *channel.Hello, certificat return errors.New("no certificates provided, unable to verify dialer") } - config := self.routerId.ServerTLSConfig() - - opts := x509.VerifyOptions{ - Roots: config.RootCAs, - Intermediates: x509.NewCertPool(), - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - } - dialedRouterId, ok := hello.Headers[LinkDialedRouterId] if ok { @@ -54,15 +47,9 @@ func (self *ConnectionHandler) HandleConnection(hello *channel.Hello, certificat } } - var errorList []error - - for _, cert := range certificates { - if _, err := cert.Verify(opts); err == nil { - return nil - } else { - errorList = append(errorList, err) - } + if _, err := cert.VerifyLeafCertChain(self.routerId.CA(), certificates); err != nil { + return fmt.Errorf("unable to verify dialing router: %w", err) } - return errors.Join(errorList...) + return nil } diff --git a/router/xlink_transport/connect_test.go b/router/xlink_transport/connect_test.go new file mode 100644 index 000000000..4eb496d18 --- /dev/null +++ b/router/xlink_transport/connect_test.go @@ -0,0 +1,150 @@ +/* + (c) Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xlink_transport + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/identity" + "github.com/stretchr/testify/require" +) + +// caPoolIdentity is a minimal identity.Identity whose only useful method is CA. The link verification +// path consults nothing else on the identity, so the remaining interface methods are left to the +// embedded nil interface (never called on the paths exercised here). +type caPoolIdentity struct { + identity.Identity + roots *x509.CertPool +} + +func (f *caPoolIdentity) CA() *x509.CertPool { return f.roots } + +func ltPool(certs ...*x509.Certificate) *x509.CertPool { + p := x509.NewCertPool() + for _, c := range certs { + p.AddCert(c) + } + return p +} + +type ltCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var ltSerial int64 + +func ltMkCert(t *testing.T, cn string, isCA bool, signer *ltCertAndKey) *ltCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + ltSerial++ + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(ltSerial), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + } + if isCA { + tmpl.IsCA = true + tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + } else { + tmpl.KeyUsage = x509.KeyUsageDigitalSignature + tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return <CertAndKey{cert: c, key: key} +} + +func ltHandler(roots *x509.CertPool, token string) *ConnectionHandler { + return &ConnectionHandler{routerId: &identity.TokenId{Identity: &caPoolIdentity{roots: roots}, Token: token}} +} + +// Test_ConnectionHandler_RejectsUntrustedDialer covers incoming link verification: the dialing router +// must present a leaf that chains to the CA. A self-signed leaf, alone or backed by a scraped +// CA-chained filler certificate, must be rejected - verification is bound to the leaf, not to "some +// presented certificate chains". +func Test_ConnectionHandler_RejectsUntrustedDialer(t *testing.T) { + req := require.New(t) + + root := ltMkCert(t, "root", true, nil) + inter := ltMkCert(t, "int", true, root) + roots := ltPool(root.cert, inter.cert) + handler := ltHandler(roots, "router1") + + legit := ltMkCert(t, "legit-router", false, inter) + forged := ltMkCert(t, "forged", false, nil) + + hello := &channel.Hello{Headers: channel.Headers{}} + + req.Error(handler.HandleConnection(hello, nil), "no certificates must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert}), + "self-signed leaf that does not chain to the CA must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert, legit.cert}), + "self-signed leaf backed by a scraped CA-chained filler cert must be rejected") +} + +func Test_ConnectionHandler_AcceptsTrustedDialer(t *testing.T) { + req := require.New(t) + + root := ltMkCert(t, "root", true, nil) + inter := ltMkCert(t, "int", true, root) + roots := ltPool(root.cert, inter.cert) + handler := ltHandler(roots, "router1") + + legit := ltMkCert(t, "legit-router", false, inter) + req.NoError(handler.HandleConnection(&channel.Hello{Headers: channel.Headers{}}, []*x509.Certificate{legit.cert}), + "a leaf chaining to the CA must be accepted") +} + +// Test_ConnectionHandler_DialedRouterIdMismatch verifies the dial is rejected when it names a different +// target router, even if the presented certificate is otherwise valid. +func Test_ConnectionHandler_DialedRouterIdMismatch(t *testing.T) { + req := require.New(t) + + root := ltMkCert(t, "root", true, nil) + inter := ltMkCert(t, "int", true, root) + roots := ltPool(root.cert, inter.cert) + handler := ltHandler(roots, "router1") + legit := ltMkCert(t, "legit-router", false, inter) + + mismatch := channel.Headers{} + mismatch.PutStringHeader(LinkDialedRouterId, "some-other-router") + req.Error(handler.HandleConnection(&channel.Hello{Headers: mismatch}, []*x509.Certificate{legit.cert}), + "a link dial meant for a different router must be rejected") + + match := channel.Headers{} + match.PutStringHeader(LinkDialedRouterId, "router1") + req.NoError(handler.HandleConnection(&channel.Hello{Headers: match}, []*x509.Certificate{legit.cert}), + "a matching dialed router id with a trusted cert must be accepted") +} diff --git a/tests/api_client_client.go b/tests/api_client_client.go index b37097534..c837a837b 100644 --- a/tests/api_client_client.go +++ b/tests/api_client_client.go @@ -420,6 +420,58 @@ func (helper *ClientHelperClient) GetCurrentApiSessionDetail() (*rest_model.Curr return resp.Payload.Data, nil } +// CreateCurrentApiSessionCertificate generates a new key and CSR, then submits it to +// create an API session certificate for the currently authenticated API Session. +func (helper *ClientHelperClient) CreateCurrentApiSessionCertificate() (*rest_model.CurrentAPISessionCertificateCreateResponse, error) { + request, err := certtools.NewCertRequest(map[string]string{ + "C": "US", "O": "NetFoundry-API-Test", "CN": uuid.NewString(), + }, nil) + + if err != nil { + return nil, fmt.Errorf("could not create base CSR values: %w", err) + } + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("could not generate private key: %w", err) + } + + csr, err := x509.CreateCertificateRequest(rand.Reader, request, privateKey) + if err != nil { + return nil, fmt.Errorf("could not create CSR: %w", err) + } + + csrPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})) + + params := &clientCurrentApiSession.CreateCurrentAPISessionCertificateParams{ + SessionCertificate: &rest_model.CurrentAPISessionCertificateCreate{ + Csr: &csrPem, + }, + } + + resp, err := helper.API.CurrentAPISession.CreateCurrentAPISessionCertificate(params, nil) + + if err != nil { + return nil, fmt.Errorf("could not create current api session certificate: %w", rest_util.WrapErr(err)) + } + + return resp.Payload.Data, nil +} + +// ListCurrentApiSessionCertificates returns the API session certificates visible to the +// currently authenticated API Session. +func (helper *ClientHelperClient) ListCurrentApiSessionCertificates() (rest_model.CurrentAPISessionCertificateList, error) { + params := &clientCurrentApiSession.ListCurrentAPISessionCertificatesParams{} + + resp, err := helper.API.CurrentAPISession.ListCurrentAPISessionCertificates(params, nil) + + if err != nil { + return nil, fmt.Errorf("could not list current api session certificates: %w", rest_util.WrapErr(err)) + } + + return resp.Payload.Data, nil +} + func (helper *ClientHelperClient) GetTotpMfa() (*rest_model.DetailMfa, error) { params := &clientCurrentIdentity.DetailMfaParams{} diff --git a/tests/api_client_management.go b/tests/api_client_management.go index a7e151bc7..0ee473d8e 100644 --- a/tests/api_client_management.go +++ b/tests/api_client_management.go @@ -121,6 +121,21 @@ func (helper *ManagementHelperClient) GetIdentity(identityId string) (*rest_mode return resp.Payload.Data, nil } +// RequireIdentitySdkInfoUpdated polls until the identity reports appId as its SDK app id, returning the +// identity as read and failing the test if it does not appear within 10 seconds. The controller applies +// sdk/env info updates on a background queue, so they are not guaranteed to be visible by the time +// authentication returns. +func (helper *ManagementHelperClient) RequireIdentitySdkInfoUpdated(identityId string, appId string) *rest_model.IdentityDetail { + var identityDetail *rest_model.IdentityDetail + helper.testCtx.Req.Eventually(func() bool { + var err error + identityDetail, err = helper.GetIdentity(identityId) + return err == nil && identityDetail.SdkInfo != nil && identityDetail.SdkInfo.AppID == appId + }, 10*time.Second, 50*time.Millisecond, "identity %s sdk info was not updated", identityId) + + return identityDetail +} + func (helper *ManagementHelperClient) CreateEnrollmentOtt(identityId *string, expiresAt *time.Time) (*rest_model.CreateLocation, error) { var expAt *strfmt.DateTime diff --git a/tests/api_session_certificates_test.go b/tests/api_session_certificates_test.go index 6d462d7bd..f31a09225 100644 --- a/tests/api_session_certificates_test.go +++ b/tests/api_session_certificates_test.go @@ -306,6 +306,132 @@ func Test_Api_Session_Certs(t *testing.T) { }) } +// Test_Api_Session_Certs_Scoped_To_Api_Session locks in that listing the current API Session's +// certificates only ever returns certificates belonging to the requesting API Session. The list +// was previously unscoped, so any authenticated identity could read every other identity's API +// session certificates. +func Test_Api_Session_Certs_Scoped_To_Api_Session(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + + adminManagementClient := ctx.NewEdgeManagementApi(nil) + adminCreds := edge_apis.NewUpdbCredentials(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password) + + adminApiSession, err := adminManagementClient.Authenticate(adminCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(adminApiSession) + + // the admin's own api session certificate is a cert that must never show up in another + // api session's list + adminClient := ctx.NewEdgeClientApi(nil) + _, err = adminClient.Authenticate(adminCreds, nil) + ctx.Req.NoError(err) + + adminCert, err := adminClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(adminCert.ID) + + t.Run("another identity's api session certificates are not listed", func(t *testing.T) { + ctx.testContextChanged(t) + + _, firstCreds, err := adminManagementClient.CreateAndEnrollOttIdentity(false) + ctx.Req.NoError(err) + ctx.Req.NotNil(firstCreds) + + _, secondCreds, err := adminManagementClient.CreateAndEnrollOttIdentity(false) + ctx.Req.NoError(err) + ctx.Req.NotNil(secondCreds) + + firstClient := ctx.NewEdgeClientApi(nil) + firstApiSession, err := firstClient.Authenticate(firstCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(firstApiSession) + + secondClient := ctx.NewEdgeClientApi(nil) + secondApiSession, err := secondClient.Authenticate(secondCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(secondApiSession) + + firstCert, err := firstClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(firstCert.ID) + + secondCert, err := secondClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(secondCert.ID) + + firstList, err := firstClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + firstIds := apiSessionCertificateIds(firstList) + ctx.Req.Contains(firstIds, firstCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(firstIds, secondCert.ID, "expected another identity's api session certificate to not be listed") + ctx.Req.NotContains(firstIds, adminCert.ID, "expected the admin's api session certificate to not be listed") + + secondList, err := secondClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + secondIds := apiSessionCertificateIds(secondList) + ctx.Req.Contains(secondIds, secondCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(secondIds, firstCert.ID, "expected another identity's api session certificate to not be listed") + ctx.Req.NotContains(secondIds, adminCert.ID, "expected the admin's api session certificate to not be listed") + }) + + t.Run("a second api session for the same identity does not list the first api session's certificates", func(t *testing.T) { + ctx.testContextChanged(t) + + _, idCreds, err := adminManagementClient.CreateAndEnrollOttIdentity(false) + ctx.Req.NoError(err) + ctx.Req.NotNil(idCreds) + + firstClient := ctx.NewEdgeClientApi(nil) + firstApiSession, err := firstClient.Authenticate(idCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(firstApiSession) + + secondClient := ctx.NewEdgeClientApi(nil) + secondApiSession, err := secondClient.Authenticate(idCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(secondApiSession) + + firstCert, err := firstClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(firstCert.ID) + + secondCert, err := secondClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(secondCert.ID) + + ctx.Req.NotEqual(firstCert.ID, secondCert.ID) + + firstList, err := firstClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + // scoping is per api session, not per identity, so the same identity's other + // api session certificate must not appear + firstIds := apiSessionCertificateIds(firstList) + ctx.Req.Contains(firstIds, firstCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(firstIds, secondCert.ID, "expected the same identity's other api session certificate to not be listed") + + secondList, err := secondClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + secondIds := apiSessionCertificateIds(secondList) + ctx.Req.Contains(secondIds, secondCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(secondIds, firstCert.ID, "expected the same identity's other api session certificate to not be listed") + }) +} + +// apiSessionCertificateIds returns the ids of the given API session certificates. +func apiSessionCertificateIds(certs rest_model.CurrentAPISessionCertificateList) []string { + var ids []string + for _, cert := range certs { + ids = append(ids, *cert.ID) + } + return ids +} + func generateCsr() ([]byte, crypto.PrivateKey, error) { p384 := elliptic.P384() pfxlog.Logger().Infof("generating %s EC key", p384.Params().Name) diff --git a/tests/api_session_totp_token_test.go b/tests/api_session_totp_token_test.go index 5167bb449..fa4c44e78 100644 --- a/tests/api_session_totp_token_test.go +++ b/tests/api_session_totp_token_test.go @@ -160,7 +160,9 @@ func Test_API_Session_TOTP_Tokens(t *testing.T) { code := adminTotpProvider.Code() + beforeTotpCreate := time.Now() totpToken, err := adminManagementClient.GetTotpToken(code) + afterTotpCreate := time.Now() ctx.Req.NoError(err) ctx.Req.NotNil(totpToken) ctx.Req.NotNil(totpToken.Token) @@ -191,8 +193,8 @@ func Test_API_Session_TOTP_Tokens(t *testing.T) { ctx.NoError(err) ctx.NotNil(issuedAt) - delta := issuedAt.Time.UTC().Sub(accessClaims.IssuedAt.AsTime().UTC()).Abs() - ctx.True(delta < 2*time.Millisecond) + // The serialized iat truncates to whole seconds, so the lower bound must too. + ctx.Req.WithinRange(issuedAt.Time, beforeTotpCreate.Truncate(time.Second), afterTotpCreate) ctx.Equal(accessClaims.ApiSessionId, totpClaims.ApiSessionId) diff --git a/tests/auth_external_jwt_signer_test.go b/tests/auth_external_jwt_signer_test.go index 1c094d2fa..02c2b84db 100644 --- a/tests/auth_external_jwt_signer_test.go +++ b/tests/auth_external_jwt_signer_test.go @@ -19,11 +19,14 @@ package tests import ( + "crypto" + "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "net" "net/http" + "os" "strconv" "sync" "testing" @@ -53,6 +56,7 @@ type jwksServer struct { mutex sync.Mutex requestCount int listener net.Listener + tlsCert *tls.Certificate } func newJwksServer(certificates []*x509.Certificate) *jwksServer { @@ -67,6 +71,16 @@ func newJwksServer(certificates []*x509.Certificate) *jwksServer { return srv } +// newTlsJwksServer returns a jwksServer that serves its JWKS over HTTPS using tlsCert as its +// server certificate. Callers must ensure the controller trusts tlsCert (see the root CA added +// to http.DefaultTransport in the overlapping-kid test) for the controller's JWKS fetch to succeed. +func newTlsJwksServer(tlsCert *tls.Certificate, certificates []*x509.Certificate) *jwksServer { + srv := newJwksServer(certificates) + srv.tlsCert = tlsCert + srv.server.TLSConfig = &tls.Config{Certificates: []tls.Certificate{*tlsCert}} + return srv +} + func (js *jwksServer) AddCertificate(certificate *x509.Certificate) { js.mutex.Lock() defer js.mutex.Unlock() @@ -87,7 +101,11 @@ func (js *jwksServer) RemoveCertificate(certificate *x509.Certificate) { } func (js *jwksServer) GetJwksUrl() string { - return "http://localhost:" + strconv.Itoa(js.port) + "/jwks" + scheme := "http" + if js.tlsCert != nil { + scheme = "https" + } + return scheme + "://localhost:" + strconv.Itoa(js.port) + "/jwks" } func (js *jwksServer) GetRequestCount() int { @@ -107,7 +125,11 @@ func (js *jwksServer) Start() error { js.port = listener.Addr().(*net.TCPAddr).Port js.mutex.Unlock() go func() { - _ = js.server.Serve(listener) + if js.tlsCert != nil { + _ = js.server.ServeTLS(listener, "", "") + } else { + _ = js.server.Serve(listener) + } }() return nil @@ -1036,3 +1058,216 @@ func Test_Authenticate_External_Jwt(t *testing.T) { }) }) } + +// Test_Authenticate_External_Jwt_Overlapping_Kids reproduces intermittent primary ext-jwt +// authentication failures that occur when multiple external JWT signers expose the same key ID +// (kid), as happens when several signers draw from a shared signing-key pool (e.g. multiple +// Entra tenants using Microsoft's keys). Token-to-issuer binding must be disambiguated by the +// token's iss claim so a shared kid resolves deterministically to the correct signer, and a +// disabled signer that happens to share a kid must not poison resolution for an enabled one. +func Test_Authenticate_External_Jwt_Overlapping_Kids(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + // The controller fetches JWKS over the default HTTP transport. Trust the test PKI root so the + // controller accepts the HTTPS JWKS providers below, which serve using the controller's own + // server certificate. Restore the prior transport config when the test completes. + rootPem, err := os.ReadFile("testdata/pki/root/certs/root.cert") + ctx.Req.NoError(err) + + rootPool, err := x509.SystemCertPool() + if err != nil || rootPool == nil { + rootPool = x509.NewCertPool() + } + ctx.Req.True(rootPool.AppendCertsFromPEM(rootPem), "expected the test root CA to be added to the trust pool") + + httpTransport := http.DefaultTransport.(*http.Transport) + priorTlsConfig := httpTransport.TLSClientConfig + httpTransport.TLSClientConfig = &tls.Config{RootCAs: rootPool} + defer func() { httpTransport.TLSClientConfig = priorTlsConfig }() + + serverTlsCert, err := tls.LoadX509KeyPair("testdata/pki/ctrl1/certs/server.chain.pem", "testdata/pki/ctrl1/keys/server.key") + ctx.Req.NoError(err) + + adminIdentityId := *ctx.AdminManagementSession.AuthResponse.IdentityID + + t.Run("two enabled signers sharing a kid authenticate deterministically by issuer", func(t *testing.T) { + ctx.testContextChanged(t) + + // One signing key and kid, served by two independent HTTPS JWKS providers - the shared + // signing-key pool scenario. + sharedCert, sharedKey := newSelfSignedCert("shared-jwks-pool-" + uuid.NewString()) + sharedKid := sharedCert.Subject.CommonName + + jwksServer1 := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(jwksServer1.Start()) + defer func() { _ = jwksServer1.Stop() }() + + jwksServer2 := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(jwksServer2.Start()) + defer func() { _ = jwksServer2.Stop() }() + + signer1Iss := "iss-shared-kid-1-" + uuid.NewString() + signer1Aud := "aud-shared-kid-1-" + uuid.NewString() + signer1Endpoint := strfmt.URI(jwksServer1.GetJwksUrl()) + signer1Env := &rest_model.CreateEnvelope{} + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &signer1Endpoint, + Enabled: ToPtr(true), + Name: ToPtr("Overlapping Kid Signer 1 - " + uuid.NewString()), + Issuer: ToPtr(signer1Iss), + Audience: ToPtr(signer1Aud), + }).SetResult(signer1Env).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + signer2Iss := "iss-shared-kid-2-" + uuid.NewString() + signer2Aud := "aud-shared-kid-2-" + uuid.NewString() + signer2Endpoint := strfmt.URI(jwksServer2.GetJwksUrl()) + signer2Env := &rest_model.CreateEnvelope{} + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &signer2Endpoint, + Enabled: ToPtr(true), + Name: ToPtr("Overlapping Kid Signer 2 - " + uuid.NewString()), + Issuer: ToPtr(signer2Iss), + Audience: ToPtr(signer2Aud), + }).SetResult(signer2Env).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.AuthPolicyPatch{ + Primary: &rest_model.AuthPolicyPrimaryPatch{ + ExtJWT: &rest_model.AuthPolicyPrimaryExtJWTPatch{ + Allowed: ToPtr(true), + AllowedSigners: []string{signer1Env.Data.ID, signer2Env.Data.ID}, + }, + }, + }).Patch("/auth-policies/default") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode(), string(resp.Body())) + + // External JWT signer creation events are processed asynchronously, so wait until signer 1's + // JWKS has resolved into the issuer cache before asserting deterministic behavior. + ctx.Req.Eventually(func() bool { + code, err := authenticateWithSignedExtJwt(ctx, signer1Iss, signer1Aud, adminIdentityId, sharedKid, sharedKey) + return err == nil && code == http.StatusOK + }, 10*time.Second, 100*time.Millisecond, "signer 1 should become usable for primary authentication") + + // Tokens are issued only by signer 1 (its iss/aud), but the kid is shared with signer 2. + // Kid-first binding over a non-deterministically ordered map would bind some requests to + // signer 2 and reject them on the issuer mismatch. Repeat enough times that a single wrong + // binding is overwhelmingly likely under the pre-fix behavior. + const attempts = 25 + for i := 0; i < attempts; i++ { + code, err := authenticateWithSignedExtJwt(ctx, signer1Iss, signer1Aud, adminIdentityId, sharedKid, sharedKey) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, code, "attempt %d: a token issued by signer 1 must authenticate regardless of a kid shared with signer 2", i) + } + }) + + t.Run("a disabled signer sharing a kid does not poison an enabled signer", func(t *testing.T) { + ctx.testContextChanged(t) + + // One signing key and kid served by two HTTPS JWKS providers - one signer enabled, one + // disabled. A disabled signer that shares a kid must not capture the binding and suppress + // issuer-string resolution for the enabled signer. + sharedCert, sharedKey := newSelfSignedCert("shared-disabled-poison-" + uuid.NewString()) + sharedKid := sharedCert.Subject.CommonName + + enabledJwksServer := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(enabledJwksServer.Start()) + defer func() { _ = enabledJwksServer.Stop() }() + + disabledJwksServer := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(disabledJwksServer.Start()) + defer func() { _ = disabledJwksServer.Stop() }() + + enabledIss := "iss-poison-enabled-" + uuid.NewString() + enabledAud := "aud-poison-enabled-" + uuid.NewString() + enabledEndpoint := strfmt.URI(enabledJwksServer.GetJwksUrl()) + enabledEnv := &rest_model.CreateEnvelope{} + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &enabledEndpoint, + Enabled: ToPtr(true), + Name: ToPtr("Poison - Enabled - " + uuid.NewString()), + Issuer: ToPtr(enabledIss), + Audience: ToPtr(enabledAud), + }).SetResult(enabledEnv).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + disabledIss := "iss-poison-disabled-" + uuid.NewString() + disabledAud := "aud-poison-disabled-" + uuid.NewString() + disabledEndpoint := strfmt.URI(disabledJwksServer.GetJwksUrl()) + disabledEnv := &rest_model.CreateEnvelope{} + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &disabledEndpoint, + Enabled: ToPtr(false), + Name: ToPtr("Poison - Disabled - " + uuid.NewString()), + Issuer: ToPtr(disabledIss), + Audience: ToPtr(disabledAud), + }).SetResult(disabledEnv).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.AuthPolicyPatch{ + Primary: &rest_model.AuthPolicyPrimaryPatch{ + ExtJWT: &rest_model.AuthPolicyPrimaryExtJWTPatch{ + Allowed: ToPtr(true), + AllowedSigners: []string{enabledEnv.Data.ID}, + }, + }, + }).Patch("/auth-policies/default") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode(), string(resp.Body())) + + // Wait until the enabled signer's JWKS has resolved into the issuer cache (creation events + // are processed asynchronously) before asserting deterministic behavior. + ctx.Req.Eventually(func() bool { + code, err := authenticateWithSignedExtJwt(ctx, enabledIss, enabledAud, adminIdentityId, sharedKid, sharedKey) + return err == nil && code == http.StatusOK + }, 10*time.Second, 100*time.Millisecond, "the enabled signer should become usable for primary authentication") + + // Tokens are issued by the enabled signer. Kid-first binding resolves to the disabled signer, + // which suppresses issuer-string resolution and then fails primary authentication because + // disabled signers are not accepted. Binding by issuer claim avoids that entirely. + const attempts = 25 + for i := 0; i < attempts; i++ { + code, err := authenticateWithSignedExtJwt(ctx, enabledIss, enabledAud, adminIdentityId, sharedKid, sharedKey) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, code, "attempt %d: a token issued by the enabled signer must authenticate even though a disabled signer shares its kid", i) + } + }) +} + +// authenticateWithSignedExtJwt signs an ES256 JWT with the given issuer, audience, subject, and kid +// using key, presents it as a primary ext-jwt bearer token, and returns the resulting HTTP status +// code. It does not assert, leaving the caller to decide what a given status means. +func authenticateWithSignedExtJwt(ctx *TestContext, issuer, audience, subject, kid string, key crypto.PrivateKey) (int, error) { + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.RegisteredClaims{ + Audience: []string{audience}, + ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(2 * time.Hour)}, + ID: uuid.NewString(), + IssuedAt: &jwt.NumericDate{Time: time.Now()}, + Issuer: issuer, + NotBefore: &jwt.NumericDate{Time: time.Now()}, + Subject: subject, + } + jwtToken.Header["kid"] = kid + + signed, err := jwtToken.SignedString(key) + if err != nil { + return 0, err + } + + result := &rest_model.CurrentAPISessionDetailEnvelope{} + resp, err := ctx.newAnonymousClientApiRequest().SetResult(result).SetHeader("Authorization", "Bearer "+signed).Post("/authenticate?method=ext-jwt") + if err != nil { + return 0, err + } + + return resp.StatusCode(), nil +} diff --git a/tests/auth_oidc_test.go b/tests/auth_oidc_test.go index 0a96453dc..e87ba4ca4 100644 --- a/tests/auth_oidc_test.go +++ b/tests/auth_oidc_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "testing" - "time" "github.com/go-resty/resty/v2" "github.com/golang-jwt/jwt/v5" @@ -202,10 +201,7 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { t.Run("has the correct sdk and env info", func(t *testing.T) { ctx.testContextChanged(t) - time.Sleep(time.Second) - identityDetail, err := managementHelper.GetIdentity(accessClaims.Subject) - - ctx.Req.NoError(err) + identityDetail := managementHelper.RequireIdentitySdkInfoUpdated(accessClaims.Subject, payload.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppID, identityDetail.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppVersion, identityDetail.SdkInfo.AppVersion) @@ -320,9 +316,7 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { t.Run("has the correct sdk and env info", func(t *testing.T) { ctx.testContextChanged(t) - identityDetail, err := managementHelper.GetIdentity(accessClaims.Subject) - - ctx.Req.NoError(err) + identityDetail := managementHelper.RequireIdentitySdkInfoUpdated(accessClaims.Subject, payload.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppID, identityDetail.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppVersion, identityDetail.SdkInfo.AppVersion) diff --git a/tests/ca_test.go b/tests/ca_test.go index 46bdeb0dc..05f2af045 100644 --- a/tests/ca_test.go +++ b/tests/ca_test.go @@ -10,6 +10,7 @@ import ( "github.com/Jeffail/gabs" "github.com/openziti/edge-api/rest_model" + edge_apis "github.com/openziti/sdk-golang/edge-apis" "github.com/openziti/ziti/v2/common/eid" "github.com/openziti/ziti/v2/controller/model" ) @@ -267,6 +268,38 @@ func Test_CA(t *testing.T) { ctx.Req.False(*enrolledSession.AuthResponse.IsCertExtendable, "expected isCertExtendable on 3rd party CA certificate authentication to be false") }) + t.Run("oidc auth from CA should not be extendable", func(t *testing.T) { + ctx.testContextChanged(t) + + certCreds := edge_apis.NewCertCredentials(clientAuthenticator.certs, clientAuthenticator.key) + certCreds.CaPool = ctx.ControllerCaPool() + + clientHelper := ctx.NewEdgeClientApi(nil) + + t.Run("access token reports the certificate as not extendable", func(t *testing.T) { + ctx.testContextChanged(t) + + _, accessClaims, err := clientHelper.OidcAccessToken(certCreds) + ctx.Req.NoError(err) + ctx.Req.False(accessClaims.IsCertExtendable, "expected the z_ice claim on 3rd party CA certificate OIDC authentication to be false") + }) + + t.Run("current api session reports the certificate as not extendable", func(t *testing.T) { + ctx.testContextChanged(t) + + clientHelper.SetUseOidc(true) + + apiSession, err := clientHelper.Authenticate(certCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + currentSession, err := clientHelper.GetCurrentApiSessionDetail() + ctx.Req.NoError(err) + ctx.Req.NotNil(currentSession.IsCertExtendable) + ctx.Req.False(*currentSession.IsCertExtendable, "expected isCertExtendable on 3rd party CA certificate OIDC authentication to be false") + }) + }) + t.Run("CAs with auth disabled can no longer authenticate", func(t *testing.T) { ctx.testContextChanged(t) ca.isAuthEnabled = false diff --git a/tests/cli_tests/cli_test.go b/tests/cli_tests/cli_test.go index 5406d99ee..8d85c9354 100644 --- a/tests/cli_tests/cli_test.go +++ b/tests/cli_tests/cli_test.go @@ -269,6 +269,14 @@ func (s *cliTestState) runCLIWithContext(ctx context.Context, cmdToRun string) ( r, w, _ := os.Pipe() os.Stdout = w + // Drain the pipe while the command runs: pipe buffers are small (4KB on Windows), so a + // command producing more output than that deadlocks if reading is deferred until it exits. + outC := make(chan string, 1) + go func() { + outBytes, _ := io.ReadAll(r) + outC <- string(outBytes) + }() + v2 := cmd.NewRootCommand(os.Stdin, w, w) v2.SetArgs(strings.Split(cmdToRun, " ")) v2.SetContext(ctx) @@ -278,8 +286,7 @@ func (s *cliTestState) runCLIWithContext(ctx context.Context, cmdToRun string) ( _ = w.Close() os.Stdout = orig - outBytes, _ := io.ReadAll(r) - return string(outBytes), err + return <-outC, err } func test(t *testing.T, name string, fn func(*testing.T)) { diff --git a/tests/configsets.go b/tests/configsets.go index 285145d7d..5b18f5f60 100644 --- a/tests/configsets.go +++ b/tests/configsets.go @@ -3,12 +3,13 @@ package tests // ConfigSet describes a named collection of config files for a single test scenario. // All file paths are relative to the tests/ working directory. type ConfigSet struct { - Name string // display name matching the testdata/configs subdirectory - CtrlConfig string // controller config file; empty if the set does not define one - EdgeRouter string // edge router config; empty if the set does not define one - TunnelerRouter string // tunneler-enabled edge router config; empty if not defined - TransitRouter string // transit router config; empty if not defined - FabricRouters []string // fabric-only router configs, ordered by 1-based index + Name string // display name matching the testdata/configs subdirectory + CtrlConfig string // controller config file; empty if the set does not define one + PeerCtrlConfigs []string // additional cluster-member controller configs, joined to the primary by StartHaCluster + EdgeRouter string // edge router config; empty if the set does not define one + TunnelerRouter string // tunneler-enabled edge router config; empty if not defined + TransitRouter string // transit router config; empty if not defined + FabricRouters []string // fabric-only router configs, ordered by 1-based index } // DefaultATS is the standard full-stack config set used by the majority of the @@ -44,6 +45,25 @@ var DisabledOidcAutoBinding = ConfigSet{ CtrlConfig: "testdata/configs/disabled-oidc-auto-binding/ctrl.yml", } +// Ha3DataDir is the parent raft data directory used by the Ha3 config set. It is cleaned by +// StartHaCluster before each run and must contain the cluster.dataDir of every ha-3 controller. +const Ha3DataDir = "testdata/ha-3-data" + +// Ha3 is a three-controller raft cluster config set whose edge signing CA root is distinct from +// the ctrl-channel root CA. Each controller signs identity certs with its own intermediate under +// the shared signing root. Used to exercise first-party client cert validation when the signing +// CA and ctrl-channel CA differ, including certs issued by a controller other than the one a +// router is subscribed to. +var Ha3 = ConfigSet{ + Name: "ha-3", + CtrlConfig: "testdata/configs/ha-3/ctrl1.yml", + PeerCtrlConfigs: []string{ + "testdata/configs/ha-3/ctrl2.yml", + "testdata/configs/ha-3/ctrl3.yml", + }, + EdgeRouter: "testdata/configs/ha-3/edge-router.yml", +} + // DualOidcServers is a controller-only config set with two web server entries, each // on a different port and each hosting the edge-oidc API. Used to verify that the OIDC // discovery document returns issuer-specific endpoint URLs that reflect the port the diff --git a/tests/context.go b/tests/context.go index cdd5adf26..9d2a5fc1f 100644 --- a/tests/context.go +++ b/tests/context.go @@ -123,6 +123,7 @@ type TestContext struct { edgeRouterEntity *edgeRouter transitRouterEntity *transitRouter routers []*router.Router + peerControllers []*peerController testing *testing.T LogLevel string ControllerConfig *config.Config @@ -438,14 +439,14 @@ func (ctx *TestContext) StartServer() *ControllerHelper { // single test override settings (e.g., OIDC token durations) without affecting // other tests that call StartServer with the shared default config. func (ctx *TestContext) StartServerWithConfigModifier(modifier func(*config.Config)) *ControllerHelper { - return ctx.startServerWith("testdata/default.db", true, modifier) + return ctx.startServerWith("testdata/default.db", true, modifier, false) } func (ctx *TestContext) StartServerFor(testDb string, clean bool) *ControllerHelper { - return ctx.startServerWith(testDb, clean, nil) + return ctx.startServerWith(testDb, clean, nil, false) } -func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func(*config.Config)) *ControllerHelper { +func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func(*config.Config), initAdminAfterRun bool) *ControllerHelper { if ctx.LogLevel != "" { if level, err := logrus.ParseLevel(ctx.LogLevel); err == nil { logrus.StandardLogger().SetLevel(level) @@ -491,14 +492,14 @@ func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func ctx.EdgeController.Initialize() - err = ctx.EdgeController.AppEnv.Managers.Identity.InitializeDefaultAdmin(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password, eid.New()) - if err != nil { - log.WithError(err).Warn("error during initialize admin") + // In raft/cluster mode the default admin is created after Run, mirroring `ziti agent cluster + // init` against a live controller: InitializeDefaultAdmin bootstraps raft and triggers self + // registration, which captures API addresses from the running xweb, so it must run once xweb is + // fully up. Non-raft controllers initialize the admin before Run as before. + if !initAdminAfterRun { + ctx.initializeDefaultAdmin() } - logrus.Infof("default admin - username: %v", ctx.AdminAuthenticator.Username) - logrus.Infof("default admin - password: %v", ctx.AdminAuthenticator.Password) - ctx.EdgeController.Run() go func() { err = ctx.fabricController.Run() @@ -507,9 +508,25 @@ func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func err = ctx.waitForRestAPIPort(time.Minute * 5) ctx.Req.NoError(err) + if initAdminAfterRun { + ctx.initializeDefaultAdmin() + } + return &ControllerHelper{Controller: ctx.EdgeController} } +// initializeDefaultAdmin creates the default admin identity. In raft mode this also bootstraps the +// cluster (via Dispatcher.Bootstrap) and waits for leadership before the admin is created. +func (ctx *TestContext) initializeDefaultAdmin() { + err := ctx.EdgeController.AppEnv.Managers.Identity.InitializeDefaultAdmin(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password, eid.New()) + if err != nil { + pfxlog.Logger().WithError(err).Warn("error during initialize admin") + } + + logrus.Infof("default admin - username: %v", ctx.AdminAuthenticator.Username) + logrus.Infof("default admin - password: %v", ctx.AdminAuthenticator.Password) +} + func (ctx *TestContext) createAndEnrollEdgeRouter(tunneler bool, roleAttributes ...string) *edgeRouter { ctx.requireCreateEdgeRouter(tunneler, roleAttributes...) ctx.requireEnrollEdgeRouter(tunneler, ctx.edgeRouterEntity.id) @@ -695,6 +712,7 @@ func (ctx *TestContext) RequireAdminClientApiLogin() { func (ctx *TestContext) Teardown() { pfxlog.Logger().Info("tearing down test context") ctx.shutdownRouters() + ctx.shutdownPeerControllers() if ctx.EdgeController != nil { ctx.EdgeController.Shutdown() ctx.EdgeController = nil @@ -789,6 +807,12 @@ func (ctx *TestContext) completeCaAutoEnrollmentWithName(certAuth *certAuthentic } func (ctx *TestContext) completeOttEnrollment(identityId string) *certAuthenticator { + return ctx.completeOttEnrollmentAtApiHost(identityId, ctx.ApiHost) +} + +// completeOttEnrollmentAtApiHost completes an identity's OTT enrollment against the client API +// at the given host, so cluster tests can enroll via a specific controller. +func (ctx *TestContext) completeOttEnrollmentAtApiHost(identityId string, apiHost string) *certAuthenticator { result := ctx.AdminManagementSession.requireQuery(fmt.Sprintf("identities/%v", identityId)) tokenValue := result.Path("data.enrollment.ott.token") @@ -810,11 +834,11 @@ func (ctx *TestContext) completeOttEnrollment(identityId string) *certAuthentica csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr}) - resp, err := ctx.newAnonymousClientApiRequest(). - SetBody(csrPem). + resp, err := ctx.NewRestClientWithDefaults().R(). SetHeader("content-type", "application/x-pem-file"). SetHeader("accept", "application/json"). - Post("enroll?token=" + token) + SetBody(csrPem). + Post("https://" + apiHost + EdgeClientApiPath + "/enroll?token=" + token) ctx.Req.NoError(err) ctx.logJson(resp.Body()) ctx.Req.Equal(http.StatusOK, resp.StatusCode()) diff --git a/tests/create_circuit_v1_test.go b/tests/create_circuit_v1_test.go new file mode 100644 index 000000000..e63377b9b --- /dev/null +++ b/tests/create_circuit_v1_test.go @@ -0,0 +1,99 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/sdk-golang/ziti/edge" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "google.golang.org/protobuf/proto" +) + +// Test_CreateCircuitV1_InvalidTokens drives the legacy v1 create-circuit handler with tokens that fail +// validation. Token validation runs against the request context's env, so a context built without it +// takes a nil-interface method call. That panic happens on the control channel's async dispatch +// goroutine, which has no recover, and would therefore terminate the whole controller rather than +// producing an error reply. +// +// The JWT-prefixed case is the one that matters: a token without that prefix is looked up in bolt and +// fails on the missing api-session before any env call, so it never reaches the vulnerable line. +func Test_CreateCircuitV1_InvalidTokens(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + edgeRouter := ctx.CreateEnrollAndStartEdgeRouter() + + ctrlCh := edgeRouter.GetNetworkControllers().AnyCtrlChannel() + ctx.Req.NotNil(ctrlCh) + + sendCreateCircuitV1 := func(apiSessionToken, sessionToken string) *channel.Message { + req := &edge_ctrl_pb.CreateCircuitRequest{ + ApiSessionToken: apiSessionToken, + SessionToken: sessionToken, + } + + body, err := proto.Marshal(req) + ctx.Req.NoError(err) + + msg := channel.NewMessage(int32(edge_ctrl_pb.ContentType_CreateCircuitRequestType), body) + reply, err := msg.WithTimeout(5 * time.Second).SendForReply(ctrlCh.GetDefaultSender()) + ctx.Req.NoError(err, "the controller must reply rather than die") + + return reply + } + + requireErrorReply := func(reply *channel.Message) { + ctx.Req.Equal(int32(edge_ctrl_pb.ContentType_ErrorType), reply.ContentType, + "expected an error reply, got content type %v", reply.ContentType) + } + + // "ey" is the OIDC access-token prefix, which routes validation through the token path. + t.Run("jwt-prefixed api session token", func(t *testing.T) { + ctx.NextTest(t) + requireErrorReply(sendCreateCircuitV1("eyJhbGciOiJIUzI1NiJ9.bogus.bogus", "eyJhbGciOiJIUzI1NiJ9.bogus.bogus")) + }) + + t.Run("opaque api session token", func(t *testing.T) { + ctx.NextTest(t) + requireErrorReply(sendCreateCircuitV1("not-a-real-api-session-token", "not-a-real-session-token")) + }) + + t.Run("empty tokens", func(t *testing.T) { + ctx.NextTest(t) + requireErrorReply(sendCreateCircuitV1("", "")) + }) + + // The controller must still be serving after the above; a panic on the dispatch goroutine would + // have taken the process down rather than let this run. + t.Run("controller still serving", func(t *testing.T) { + ctx.NextTest(t) + + reply := sendCreateCircuitV1("eyJhbGciOiJIUzI1NiJ9.bogus.bogus", "eyJhbGciOiJIUzI1NiJ9.bogus.bogus") + requireErrorReply(reply) + + code, found := reply.GetUint32Header(edge.ErrorCodeHeader) + ctx.Req.True(found, "error replies carry an edge error code") + ctx.Req.NotZero(code) + }) +} diff --git a/tests/create_circuit_v3_test.go b/tests/create_circuit_v3_test.go index 0797a176f..b5f18b491 100644 --- a/tests/create_circuit_v3_test.go +++ b/tests/create_circuit_v3_test.go @@ -24,6 +24,8 @@ import ( "time" "github.com/google/uuid" + "github.com/openziti/edge-api/rest_util" + edge_apis "github.com/openziti/sdk-golang/edge-apis" "github.com/openziti/sdk-golang/ziti/edge" "github.com/openziti/ziti/v2/common/ctrl_msg" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" @@ -71,11 +73,33 @@ func Test_CreateCircuitV3(t *testing.T) { terminatorWatcher.waitForTerminators(5 * time.Second) - // Create a dialer identity (has dial access via dialerRole). - dialerIdentity := ctx.AdminManagementSession.requireNewIdentity(false, "dialerRole") + managementHelper := ctx.NewEdgeManagementApi(nil) + adminCreds := edge_apis.NewUpdbCredentials(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password) + adminCreds.CaPool = ctx.ControllerCaPool() + _, err = managementHelper.Authenticate(adminCreds, nil) + ctx.Req.NoError(rest_util.WrapErr(err)) - // Create an identity with no dial access. - noAccessIdentity := ctx.AdminManagementSession.requireNewIdentity(false, "noAccessRole") + clientHelper := ctx.NewEdgeClientApi(nil) + + // The controller takes the dialing identity from the API session token's claims, so + // each identity here needs a real, authenticated token rather than just an ID. + authenticateIdentity := func(roleAttributes ...string) (string, string) { + identityDetail, certCreds, err := managementHelper.CreateAndEnrollOttIdentity(false, roleAttributes...) + ctx.Req.NoError(err) + certCreds.CaPool = ctx.ControllerCaPool() + + accessToken, claims, err := clientHelper.OidcAccessToken(certCreds) + ctx.Req.NoError(err) + ctx.Req.Equal(*identityDetail.ID, claims.Subject) + + return *identityDetail.ID, accessToken + } + + // A dialer identity, with dial access via dialerRole. + dialerIdentityId, dialerToken := authenticateIdentity("dialerRole") + + // An identity with no dial access. + noAccessIdentityId, noAccessToken := authenticateIdentity("noAccessRole") // Get the control channel from the edge router to the controller. ctrlCh := edgeRouter.GetNetworkControllers().AnyCtrlChannel() @@ -105,14 +129,22 @@ func Test_CreateCircuitV3(t *testing.T) { return ctrl_msg.DecodeCreateCircuitV3Response(msg) } + requireErrorCode := func(err error, code uint32) { + ctx.Req.Error(err) + circuitErr, ok := err.(*circuitV3Error) + ctx.Req.True(ok, "expected a controller error response, got %v", err) + ctx.Req.Equal(code, circuitErr.code, "unexpected error code, message was: %v", circuitErr.msg) + } + t.Run("successful circuit creation", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: dialerIdentity.Id, - ServiceId: svc.Id, - CircuitId: uuid.NewString(), - PeerData: map[uint32][]byte{}, + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: dialerToken, + PeerData: map[uint32][]byte{}, } resp, err := sendV3Request(req) @@ -122,46 +154,80 @@ func Test_CreateCircuitV3(t *testing.T) { ctx.Req.NotEmpty(resp.Address) }) - t.Run("invalid identity", func(t *testing.T) { + t.Run("missing api session token", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: "bogus-identity-id", + IdentityId: dialerIdentityId, ServiceId: svc.Id, CircuitId: uuid.NewString(), PeerData: map[uint32][]byte{}, } _, err := sendV3Request(req) - ctx.Req.Error(err) + requireErrorCode(err, edge.ErrorCodeInvalidApiSession) + }) + + t.Run("invalid api session token", func(t *testing.T) { + ctx.NextTest(t) + + req := &ctrl_msg.CreateCircuitV3Request{ + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: "not-a-valid-token", + PeerData: map[uint32][]byte{}, + } + + _, err := sendV3Request(req) + requireErrorCode(err, edge.ErrorCodeInvalidApiSession) + }) + + // A router must not be able to dial on behalf of an identity other than the one + // whose API session token it presents, even when that identity has dial access. + t.Run("api session token for a different identity", func(t *testing.T) { + ctx.NextTest(t) + + req := &ctrl_msg.CreateCircuitV3Request{ + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: noAccessToken, + PeerData: map[uint32][]byte{}, + } + + _, err := sendV3Request(req) + requireErrorCode(err, edge.ErrorCodeInvalidApiSession) }) t.Run("invalid service", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: dialerIdentity.Id, - ServiceId: "bogus-service-id", - CircuitId: uuid.NewString(), - PeerData: map[uint32][]byte{}, + IdentityId: dialerIdentityId, + ServiceId: "bogus-service-id", + CircuitId: uuid.NewString(), + ApiSessionToken: dialerToken, + PeerData: map[uint32][]byte{}, } _, err := sendV3Request(req) - ctx.Req.Error(err) + requireErrorCode(err, edge.ErrorCodeInvalidService) }) t.Run("identity without dial access", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: noAccessIdentity.Id, - ServiceId: svc.Id, - CircuitId: uuid.NewString(), - PeerData: map[uint32][]byte{}, + IdentityId: noAccessIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: noAccessToken, + PeerData: map[uint32][]byte{}, } _, err := sendV3Request(req) - ctx.Req.Error(err) + requireErrorCode(err, edge.ErrorCodeInvalidService) }) t.Run("duplicate circuit ID", func(t *testing.T) { @@ -170,10 +236,11 @@ func Test_CreateCircuitV3(t *testing.T) { circuitId := uuid.NewString() req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: dialerIdentity.Id, - ServiceId: svc.Id, - CircuitId: circuitId, - PeerData: map[uint32][]byte{}, + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: circuitId, + ApiSessionToken: dialerToken, + PeerData: map[uint32][]byte{}, } resp, err := sendV3Request(req) diff --git a/tests/enrollment_token_to_cert_test.go b/tests/enrollment_token_to_cert_test.go index 47bc36f90..7e7d27f6b 100644 --- a/tests/enrollment_token_to_cert_test.go +++ b/tests/enrollment_token_to_cert_test.go @@ -123,6 +123,104 @@ func Test_EnrollmentToken_ToCertificate(t *testing.T) { }) }) + t.Run("when the attribute selector is set but the claim is absent", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorAbsent := createExtJwtComponents("enroll-to-cert-attr-selector-absent") + extJwtSingerAttrSelectorAbsent.Create.EnrollToCertEnabled = true + extJwtSingerAttrSelectorAbsent.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID + extJwtSingerAttrSelectorAbsent.Create.EnrollAttributeClaimsSelector = "absent-attr-selector" + extJwtSingerAttrSelectorAbsent.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorAbsent.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorAbsent.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorAbsent.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorAbsent.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorAbsent, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim absent from the token enrolls with no role attributes rather than failing + creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) + ctx.Req.NoError(err) + ctx.Req.NotNil(creds) + ctx.Req.NotNil(creds.Key) + ctx.Req.NotEmpty(creds.Certs) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + + t.Run("when the attribute selector is set but the claim is null", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorNull := createExtJwtComponents("enroll-to-cert-attr-selector-null") + extJwtSingerAttrSelectorNull.Create.EnrollToCertEnabled = true + extJwtSingerAttrSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID + extJwtSingerAttrSelectorNull.Create.EnrollAttributeClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerAttrSelectorNull.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorNull.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim sent as null enrolls with no role attributes rather than failing + creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) + ctx.Req.NoError(err) + ctx.Req.NotNil(creds) + ctx.Req.NotNil(creds.Key) + ctx.Req.NotEmpty(creds.Certs) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + t.Run("when all selectors set with multiple attributes", func(t *testing.T) { ctx.testContextChanged(t) @@ -318,6 +416,33 @@ func Test_EnrollmentToken_ToCertificate(t *testing.T) { ctx.Req.Nil(creds) }) + t.Run("if the name claim selector resolves to a null claim", func(t *testing.T) { + extJwtSingerNameSelectorNull := createExtJwtComponents("enroll-to-cert-name-selector-null") + extJwtSingerNameSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID + extJwtSingerNameSelectorNull.Create.Enabled = ToPtr(true) + extJwtSingerNameSelectorNull.Create.EnrollToCertEnabled = true + extJwtSingerNameSelectorNull.Create.EnrollNameClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerNameSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerNameSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerNameSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerNameSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an identity cannot be created without a name, so unlike the optional attribute claim a + // null name claim rejects the enrollment + creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) + + ctx.Req.Error(err) + ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) + ctx.Req.Nil(creds) + }) + t.Run("if the name claim selector resolves to a non-string", func(t *testing.T) { extJwtSingerNameIsNumberSelectorFails := createExtJwtComponents("enroll-to-cert-name-selector-is-number-fails") extJwtSingerNameIsNumberSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID @@ -343,31 +468,6 @@ func Test_EnrollmentToken_ToCertificate(t *testing.T) { ctx.Req.Nil(creds) }) - t.Run("if the attribute claim selector does not resolve", func(t *testing.T) { - extJwtSingerAttrSelectorFails := createExtJwtComponents("enroll-to-cert-attr-selector-fails") - extJwtSingerAttrSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID - extJwtSingerAttrSelectorFails.Create.Enabled = ToPtr(true) - extJwtSingerAttrSelectorFails.Create.EnrollToCertEnabled = true - extJwtSingerAttrSelectorFails.Create.EnrollAttributeClaimsSelector = "invalid-attr-selector" - extJwtSingerAttrSelectorFails.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorFails.Create) - ctx.Req.NoError(err) - ctx.Req.NotNil(extJwtSingerAttrSelectorFails.Detail) - - enrollClaims := &claimsWithAttributes{} - enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorFails, enrollClaims) - ctx.Req.NoError(err) - ctx.Req.NotEmpty(enrollmentJwt) - - clientApi := ctx.NewEdgeClientApi(nil) - ctx.Req.NotNil(clientApi) - - creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) - - ctx.Req.Error(err) - ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) - ctx.Req.Nil(creds) - }) - t.Run("if the attribute claim selector resolves to a non-string or non-string-array", func(t *testing.T) { extJwtSingerAttrIsNumberSelectorNotString := createExtJwtComponents("enroll-to-cert-attr-selector-is-number-fails") extJwtSingerAttrIsNumberSelectorNotString.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID @@ -695,6 +795,7 @@ const ( ClaimsWithAttributesAttributeStringPropertyName = "attributeString" ClaimsWithAttributesCustomIdPropertyName = "customId" ClaimsWithAttributesCustomNamePropertyName = "customName" + ClaimsWithAttributesNullClaimPropertyName = "nullClaim" ) type claimsWithAttributes struct { @@ -704,4 +805,8 @@ type claimsWithAttributes struct { CustomId string `json:"customId,omitempty"` CustomName string `json:"customName,omitempty"` NumberValue int64 `json:"numberValue,omitempty"` + + // NullClaim has no omitempty and is never populated, so it always serializes as an explicit JSON + // null. It models an IdP that emits an optional claim as null rather than omitting it. + NullClaim *string `json:"nullClaim"` } diff --git a/tests/enrollment_token_to_token_test.go b/tests/enrollment_token_to_token_test.go index 55062d29e..ddb253b20 100644 --- a/tests/enrollment_token_to_token_test.go +++ b/tests/enrollment_token_to_token_test.go @@ -116,6 +116,100 @@ func Test_EnrollmentToken_ToToken(t *testing.T) { }) }) + t.Run("when the attribute selector is set but the claim is absent", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorAbsent := createExtJwtComponents("enroll-to-token-attr-selector-absent") + extJwtSingerAttrSelectorAbsent.Create.EnrollToTokenEnabled = true + extJwtSingerAttrSelectorAbsent.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID + extJwtSingerAttrSelectorAbsent.Create.EnrollAttributeClaimsSelector = "absent-attr-selector" + extJwtSingerAttrSelectorAbsent.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorAbsent.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorAbsent.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorAbsent.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorAbsent.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorAbsent, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim absent from the token enrolls with no role attributes rather than failing + err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) + ctx.Req.NoError(err) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + creds := edgeApis.NewJwtCredentials(enrollmentJwt) + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + + t.Run("when the attribute selector is set but the claim is null", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorNull := createExtJwtComponents("enroll-to-token-attr-selector-null") + extJwtSingerAttrSelectorNull.Create.EnrollToTokenEnabled = true + extJwtSingerAttrSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID + extJwtSingerAttrSelectorNull.Create.EnrollAttributeClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerAttrSelectorNull.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorNull.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim sent as null enrolls with no role attributes rather than failing + err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) + ctx.Req.NoError(err) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + creds := edgeApis.NewJwtCredentials(enrollmentJwt) + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + t.Run("when all selectors set with multiple attributes", func(t *testing.T) { ctx.testContextChanged(t) @@ -305,6 +399,32 @@ func Test_EnrollmentToken_ToToken(t *testing.T) { ctx.Req.Nil(creds) }) + t.Run("if the name claim selector resolves to a null claim", func(t *testing.T) { + extJwtSingerNameSelectorNull := createExtJwtComponents("enroll-to-token-name-selector-null") + extJwtSingerNameSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID + extJwtSingerNameSelectorNull.Create.Enabled = ToPtr(true) + extJwtSingerNameSelectorNull.Create.EnrollToTokenEnabled = true + extJwtSingerNameSelectorNull.Create.EnrollNameClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerNameSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerNameSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerNameSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerNameSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an identity cannot be created without a name, so unlike the optional attribute claim a + // null name claim rejects the enrollment + err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) + + ctx.Req.Error(err) + ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) + }) + t.Run("if the name claim selector resolves to a non-string", func(t *testing.T) { extJwtSingerNameIsNumberSelectorFails := createExtJwtComponents("enroll-to-token-name-selector-is-number-fails") extJwtSingerNameIsNumberSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID @@ -330,30 +450,6 @@ func Test_EnrollmentToken_ToToken(t *testing.T) { ctx.Req.Nil(creds) }) - t.Run("if the attribute claim selector does not resolve", func(t *testing.T) { - extJwtSingerAttrSelectorFails := createExtJwtComponents("enroll-to-token-attr-selector-fails") - extJwtSingerAttrSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID - extJwtSingerAttrSelectorFails.Create.Enabled = ToPtr(true) - extJwtSingerAttrSelectorFails.Create.EnrollToTokenEnabled = true - extJwtSingerAttrSelectorFails.Create.EnrollAttributeClaimsSelector = "invalid-attr-selector" - extJwtSingerAttrSelectorFails.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorFails.Create) - ctx.Req.NoError(err) - ctx.Req.NotNil(extJwtSingerAttrSelectorFails.Detail) - - enrollClaims := &claimsWithAttributes{} - enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorFails, enrollClaims) - ctx.Req.NoError(err) - ctx.Req.NotEmpty(enrollmentJwt) - - clientApi := ctx.NewEdgeClientApi(nil) - ctx.Req.NotNil(clientApi) - - err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) - - ctx.Req.Error(err) - ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) - }) - t.Run("if the attribute claim selector resolves to a non-string or non-string-array", func(t *testing.T) { extJwtSingerAttrIsNumberSelectorNotString := createExtJwtComponents("enroll-to-token-attr-selector-is-number-fails") extJwtSingerAttrIsNumberSelectorNotString.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID diff --git a/tests/external_jwt_signer_jwks_endpoint_test.go b/tests/external_jwt_signer_jwks_endpoint_test.go new file mode 100644 index 000000000..70f10089b --- /dev/null +++ b/tests/external_jwt_signer_jwks_endpoint_test.go @@ -0,0 +1,88 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "testing" + + "github.com/go-openapi/strfmt" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/foundation/v2/errorz" + "github.com/openziti/ziti/v2/common/eid" +) + +// Test_ExternalJwtSigner_JwksEndpoint covers the create-time check on an external JWT signer's +// jwksEndpoint. The controller fetches that URL itself, so an endpoint that the configured +// fetch policy would refuse is rejected up front rather than failing later during a fetch. +func Test_ExternalJwtSigner_JwksEndpoint(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + + adminManClient := ctx.NewEdgeManagementApi(nil) + adminManApiSession, err := adminManClient.Authenticate(ctx.NewAdminCredentials(), nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(adminManApiSession) + + t.Run("an endpoint pointing at the instance metadata service is rejected", func(t *testing.T) { + ctx.testContextChanged(t) + + detail, err := adminManClient.CreateExtJwtSigner(newJwksSignerCreate("http://169.254.169.254/latest/meta-data/")) + + ctx.Req.Error(err, "the metadata service must never be fetchable, regardless of configuration") + ctx.Req.ApiErrorWithCode(err, errorz.InvalidFieldCode) + ctx.Req.Nil(detail) + }) + + t.Run("an endpoint with a non-http scheme is rejected", func(t *testing.T) { + ctx.testContextChanged(t) + + detail, err := adminManClient.CreateExtJwtSigner(newJwksSignerCreate("file:///etc/passwd")) + + ctx.Req.Error(err) + ctx.Req.ApiErrorWithCode(err, errorz.InvalidFieldCode) + ctx.Req.Nil(detail) + }) + + t.Run("an endpoint with a hostname is accepted", func(t *testing.T) { + ctx.testContextChanged(t) + + // no name resolution happens at create time, the dial-time check covers a hostname + // that resolves to a blocked address + detail, err := adminManClient.CreateExtJwtSigner(newJwksSignerCreate("https://idp.example.com/.well-known/jwks.json")) + + ctx.Req.NoError(err) + ctx.Req.NotNil(detail) + }) +} + +// newJwksSignerCreate returns a uniquely named external JWT signer create payload that uses +// the given jwksEndpoint. +func newJwksSignerCreate(jwksEndpoint string) *rest_model.ExternalJWTSignerCreate { + name := eid.New() + endpoint := strfmt.URI(jwksEndpoint) + + return &rest_model.ExternalJWTSignerCreate{ + Name: ToPtr(name), + Enabled: ToPtr(true), + Issuer: ToPtr(name + "-issuer"), + Audience: ToPtr(name + "-audience"), + JwksEndpoint: &endpoint, + } +} diff --git a/tests/ha_cluster.go b/tests/ha_cluster.go new file mode 100644 index 000000000..c202f573a --- /dev/null +++ b/tests/ha_cluster.go @@ -0,0 +1,225 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "fmt" + "net/url" + "os" + "strings" + "time" + + hashiraft "github.com/hashicorp/raft" + "github.com/michaelquigley/pfxlog" + edgeApis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/ziti/v2/common/pb/cmd_pb" + "github.com/openziti/ziti/v2/controller" + "github.com/openziti/ziti/v2/controller/config" + "github.com/openziti/ziti/v2/controller/raft" + "github.com/openziti/ziti/v2/controller/server" +) + +// peerController is a secondary cluster-member controller started by StartHaCluster. The +// primary controller remains on the TestContext fields (EdgeController, ApiHost, ...) so all +// existing helpers keep working against it; peers are addressed via their own ApiHost. +type peerController struct { + fabricController *controller.Controller + EdgeController *server.Controller + ApiHost string + config *config.Config +} + +// PeerControllerApiHosts returns the API host:port of each peer controller, in +// PeerCtrlConfigs order. +func (ctx *TestContext) PeerControllerApiHosts() []string { + var result []string + for _, peer := range ctx.peerControllers { + result = append(result, peer.ApiHost) + } + return result +} + +// NewEdgeClientApiForHost returns a ClientHelperClient targeting the given API host instead of +// the primary controller's. The CA pool is shared cluster-wide. +func (ctx *TestContext) NewEdgeClientApiForHost(apiHost string, totpProvider func(chan string)) *ClientHelperClient { + if totpProvider == nil { + totpProvider = func(chan string) {} + } + apiUrl, err := url.Parse("https://" + apiHost + EdgeClientApiPath) + ctx.Req.NoError(err) + + client := edgeApis.NewClientApiClient([]*url.URL{apiUrl}, ctx.ControllerCaPool(), totpProvider) + + return &ClientHelperClient{ + ClientApiClient: client, + testCtx: ctx, + } +} + +// StartHaCluster starts the config set's primary controller plus every PeerCtrlConfigs member, +// joins them into a single raft cluster, and waits until every member is a voter and is +// registered in the Controller store. dataDir is the parent raft data directory shared by the +// set's controllers (e.g. Ha3DataDir); it is removed first so each run bootstraps fresh. +func (ctx *TestContext) StartHaCluster(dataDir string) *ControllerHelper { + err := os.RemoveAll(dataDir) + ctx.Req.NoError(err) + + helper := ctx.startServerWith("testdata/ha-unused.db", true, nil, true) + + raftCtrl, ok := ctx.fabricController.GetCommandDispatcher().(*raft.Controller) + ctx.Req.True(ok, "primary controller is not running in cluster mode") + + for _, peerConfigFile := range ctx.configSet.PeerCtrlConfigs { + ctx.startPeerController(peerConfigFile) + } + + for _, peer := range ctx.peerControllers { + ctx.Req.NotNil(peer.config.Ctrl.Options.AdvertiseAddress, "peer controller config has no ctrl advertise address") + peerCtrlAddr := (*peer.config.Ctrl.Options.AdvertiseAddress).String() + ctx.Req.NoError(ctx.waitForPort(strings.TrimPrefix(peerCtrlAddr, "tls:"), time.Minute)) + + req := &cmd_pb.AddPeerRequest{ + Addr: peerCtrlAddr, + IsVoter: true, + } + pfxlog.Logger().WithField("addr", req.Addr).Info("joining peer controller to test cluster") + + // Adding a voter can bounce leadership while replication to the new member settles, + // so joins are retried until the cluster has a stable leader again. + var joinErr error + for attempt := 0; attempt < 30; attempt++ { + if joinErr = raftCtrl.HandleAddPeer(req); joinErr == nil { + break + } + pfxlog.Logger().WithError(joinErr).WithField("addr", req.Addr).Warn("join attempt failed, retrying") + time.Sleep(time.Second) + } + ctx.Req.NoError(joinErr, "could not join peer controller to test cluster") + } + + ctx.waitForClusterReady(raftCtrl, 1+len(ctx.peerControllers), time.Minute) + + return helper +} + +// startPeerController starts one secondary cluster-member controller. Unlike the primary, no +// default admin is initialized: peers get all replicated state through raft once joined. +func (ctx *TestContext) startPeerController(configFile string) { + log := pfxlog.Logger().WithField("config", configFile) + log.Info("starting peer controller") + + cfg, err := config.LoadConfig(configFile) + ctx.Req.NoError(err) + + fabricController, err := controller.NewController(cfg, NewVersionProviderTest()) + ctx.Req.NoError(err) + + edgeController, err := server.NewController(fabricController) + ctx.Req.NoError(err) + + edgeController.Initialize() + edgeController.Run() + go func() { + ctx.Req.NoError(fabricController.Run()) + }() + + peer := &peerController{ + fabricController: fabricController, + EdgeController: edgeController, + ApiHost: cfg.Edge.Api.Address, + config: cfg, + } + ctx.peerControllers = append(ctx.peerControllers, peer) + + ctx.Req.NoError(ctx.waitForPort(peer.ApiHost, time.Minute)) + log.WithField("apiHost", peer.ApiHost).Info("peer controller started") +} + +// waitForClusterReady polls until the raft configuration lists memberCount voters and the +// Controller store holds memberCount records, so tests observe a fully-registered cluster. +func (ctx *TestContext) waitForClusterReady(raftCtrl *raft.Controller, memberCount int, timeout time.Duration) { + deadline := time.Now().Add(timeout) + var lastState string + + for { + voters := 0 + configFuture := raftCtrl.GetRaft().GetConfiguration() + if err := configFuture.Error(); err == nil { + for _, srv := range configFuture.Configuration().Servers { + if srv.Suffrage == hashiraft.Voter { + voters++ + } + } + } + + registered := 0 + var recordDescs []string + if result, err := ctx.EdgeController.AppEnv.Managers.Controller.BaseList("true limit none"); err == nil { + registered = len(result.Entities) + for _, entity := range result.Entities { + recordDescs = append(recordDescs, fmt.Sprintf("%s(%s)", entity.Id, entity.Name)) + } + } + + var peerCounts []int + for _, peer := range ctx.peerControllers { + count := -1 + if result, err := peer.EdgeController.AppEnv.Managers.Controller.BaseList("true limit none"); err == nil { + count = len(result.Entities) + } + peerCounts = append(peerCounts, count) + } + + lastState = fmt.Sprintf("voters: %v/%v, controller store records: %v/%v %v, peer store counts: %v", voters, memberCount, registered, memberCount, recordDescs, peerCounts) + if voters >= memberCount && registered >= memberCount { + pfxlog.Logger().Info("test cluster ready: " + lastState) + return + } + + if time.Now().After(deadline) { + ctx.Req.Fail("timed out waiting for cluster to become ready", lastState) + return + } + time.Sleep(100 * time.Millisecond) + } +} + +// waitForIdentityOnPeer polls until the peer controller at peerIndex can read the given +// identity, covering raft replication lag after creating state via another cluster member. +func (ctx *TestContext) waitForIdentityOnPeer(peerIndex int, identityId string, timeout time.Duration) error { + peer := ctx.peerControllers[peerIndex] + deadline := time.Now().Add(timeout) + for { + _, err := peer.EdgeController.AppEnv.Managers.Identity.Read(identityId) + if err == nil { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("identity %s did not replicate to peer controller %d within %s: %w", identityId, peerIndex, timeout, err) + } + time.Sleep(25 * time.Millisecond) + } +} + +// shutdownPeerControllers stops every peer controller started by StartHaCluster. +func (ctx *TestContext) shutdownPeerControllers() { + for _, peer := range ctx.peerControllers { + peer.EdgeController.Shutdown() + peer.fabricController.Shutdown() + } + ctx.peerControllers = nil +} diff --git a/tests/ha_cluster_formation_test.go b/tests/ha_cluster_formation_test.go new file mode 100644 index 000000000..241bd0cfa --- /dev/null +++ b/tests/ha_cluster_formation_test.go @@ -0,0 +1,15 @@ +//go:build apitests + +package tests + +import ( + "testing" +) + +// Test_HaClusterFormation verifies the multi-controller test harness forms a full raft +// cluster: every member is a voter and every member is registered in the Controller store. +func Test_HaClusterFormation(t *testing.T) { + ctx := NewTestContextWithConfigSet(t, Ha3) + defer ctx.Teardown() + ctx.StartHaCluster(Ha3DataDir) +} diff --git a/tests/ha_first_party_cert_test.go b/tests/ha_first_party_cert_test.go new file mode 100644 index 000000000..56a1d8b68 --- /dev/null +++ b/tests/ha_first_party_cert_test.go @@ -0,0 +1,245 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "crypto" + "errors" + "slices" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/openziti/edge-api/rest_management_api_client/auth_policy" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + nfpem "github.com/openziti/foundation/v2/pem" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/common/eid" + "github.com/openziti/ziti/v2/controller/oidc_auth" +) + +// extJwtTestSigner holds the signing material and claims values of a registered test +// ext-jwt-signer, so tests can mint bearer tokens the controller will accept. +type extJwtTestSigner struct { + audience string + issuer string + kid string + key crypto.PrivateKey +} + +// newJwtForIdentity mints a bearer token for the given identity, signed by the test signer. +func (s *extJwtTestSigner) newJwtForIdentity(identityId string) (string, error) { + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.RegisteredClaims{ + Audience: []string{s.audience}, + ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(2 * time.Hour)}, + ID: uuid.NewString(), + IssuedAt: &jwt.NumericDate{Time: time.Now()}, + Issuer: s.issuer, + NotBefore: &jwt.NumericDate{Time: time.Now()}, + Subject: identityId, + } + jwtToken.Header["kid"] = s.kid + return jwtToken.SignedString(s.key) +} + +// registerExtJwtSignerAllowedByDefaultPolicy creates an enabled ext-jwt-signer and patches the +// default auth policy to allow it as a primary auth method. +func registerExtJwtSignerAllowedByDefaultPolicy(managementHelper *ManagementHelperClient) (*extJwtTestSigner, error) { + signerCert, signerKey := newSelfSignedCert("Test Ext-Jwt Signer") + testSigner := &extJwtTestSigner{ + audience: uuid.NewString(), + issuer: uuid.NewString(), + kid: uuid.NewString(), + key: signerKey, + } + + createSigner := external_jwt_signer.NewCreateExternalJWTSignerParams() + createSigner.ExternalJWTSigner = &rest_model.ExternalJWTSignerCreate{ + CertPem: ToPtr(nfpem.EncodeToString(signerCert)), + Enabled: ToPtr(true), + Name: ToPtr("Test Ext-Jwt Signer"), + Kid: ToPtr(testSigner.kid), + Issuer: ToPtr(testSigner.issuer), + Audience: ToPtr(testSigner.audience), + } + signerCreateResp, err := managementHelper.API.ExternalJWTSigner.CreateExternalJWTSigner(createSigner, nil) + if err != nil { + return nil, rest_util.WrapErr(err) + } + + patchPolicy := auth_policy.NewPatchAuthPolicyParams() + patchPolicy.ID = "default" + patchPolicy.AuthPolicy = &rest_model.AuthPolicyPatch{ + Primary: &rest_model.AuthPolicyPrimaryPatch{ + ExtJWT: &rest_model.AuthPolicyPrimaryExtJWTPatch{ + Allowed: ToPtr(true), + AllowedSigners: []string{signerCreateResp.Payload.Data.ID}, + }, + }, + } + if _, err = managementHelper.API.AuthPolicy.PatchAuthPolicy(patchPolicy, nil); err != nil { + return nil, rest_util.WrapErr(err) + } + + return testSigner, nil +} + +// dialServiceWithFirstPartyCert authenticates via ext-jwt while presenting the given +// first-party cert at the TLS layer, verifies the session carries no cert fingerprints (so +// router-side acceptance must come from the first-party origin check, not a fingerprint +// match), and dials the service, requiring an edge router connection. +func dialServiceWithFirstPartyCert(ctx *TestContext, testSigner *extJwtTestSigner, identityId string, certAuth *certAuthenticator, serviceName string) error { + jwtStr, err := testSigner.newJwtForIdentity(identityId) + if err != nil { + return err + } + + // An enrolled client trusts the controller's full CA bundle, which includes the edge + // signing CA chain that issues router server certs, not just the ctrl-channel root. + caPool := ctx.ControllerCaPool().Clone() + caPool.AppendCertsFromPEM(ctx.ControllerConfig.Edge.CaPems()) + + baseCreds := edge_apis.NewJwtCredentials(jwtStr) + baseCreds.CaPool = caPool + creds := &extJwtCredsWithFirstPartyCert{ + JwtCredentials: baseCreds, + certs: certAuth.certs, + key: certAuth.key, + } + + clientContext, err := ziti.NewContext(&ziti.Config{ + ZtAPI: "https://" + ctx.ApiHost + EdgeClientApiPath, + Credentials: creds, + }) + if err != nil { + return err + } + defer clientContext.Close() + + if err = clientContext.Authenticate(); err != nil { + return err + } + + ctxImpl, ok := clientContext.(*ziti.ContextImpl) + if !ok { + return errors.New("expected *ziti.ContextImpl from ziti.NewContext") + } + oidcSession, ok := ctxImpl.CtrlClt.GetCurrentApiSession().(*edge_apis.ApiSessionOidc) + if !ok { + return errors.New("expected OIDC api session; SDK fell back to legacy, this test requires OIDC mode") + } + claims := &common.AccessClaims{} + if _, _, err = jwt.NewParser().ParseUnverified(oidcSession.OidcTokens.AccessToken, claims); err != nil { + return err + } + if !slices.Contains(claims.AuthenticationMethodsReferences, oidc_auth.AuthMethodExtJwt) { + return errors.New("expected ext-jwt authentication method reference on the session") + } + if len(claims.CertFingerprints) > 0 { + return errors.New("ext-jwt auth must not bind the first-party cert into the session fingerprints") + } + + connectChan := make(chan struct{}, 1) + clientContext.Events().AddRouterConnectedListener(func(ziti.Context, string, string) { + select { + case connectChan <- struct{}{}: + default: + } + }) + conn, _ := clientContext.Dial(serviceName) + defer func() { + if conn != nil { + _ = conn.Close() + } + }() + + select { + case <-connectChan: + return nil + case <-time.After(5 * time.Second): + return errors.New("router connection did not occur within 5 seconds") + } +} + +// Test_FirstPartyCert_SeparateSigningCa runs a three-controller cluster whose edge signing CA +// root is distinct from the ctrl-channel root CA. An identity authenticates via ext-jwt (so no +// cert fingerprints bind to the session) and attaches to an edge router presenting its +// first-party enrollment cert. The router must recognize the cert as first-party via the +// signing CA published in the router data model, for certs issued by the controller the router +// is subscribed to as well as by peer controllers. +func Test_FirstPartyCert_SeparateSigningCa(t *testing.T) { + ctx := NewTestContextWithConfigSet(t, Ha3) + defer ctx.Teardown() + ctx.StartHaCluster(Ha3DataDir) + + managementHelper := ctx.NewEdgeManagementApi(nil) + adminCreds := ctx.NewAdminCredentials() + _, err := managementHelper.Authenticate(adminCreds, nil) + ctx.Req.NoError(rest_util.WrapErr(err)) + ctx.RequireAdminManagementApiLogin() + + testService := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll("smartrouting") + ctx.CreateEnrollAndStartEdgeRouter() + + testSigner, err := registerExtJwtSignerAllowedByDefaultPolicy(managementHelper) + ctx.Req.NoError(err) + + t.Run("cert enrolled via the primary controller is accepted by the edge router", func(t *testing.T) { + ctx.NextTest(t) + + identityId, certAuth := ctx.AdminManagementSession.requireCreateIdentityOttEnrollment(eid.New(), false) + + // The enrollment cert must chain to the signing root, not the ctrl-channel root. + ctx.Req.Equal("Controller One Edge Signing Cert", certAuth.certs[0].Issuer.CommonName) + + ctx.Req.NoError(dialServiceWithFirstPartyCert(ctx, testSigner, identityId, certAuth, testService.Name)) + }) + + t.Run("cert enrolled via a peer controller is accepted by the edge router", func(t *testing.T) { + ctx.NextTest(t) + + identityId := ctx.AdminManagementSession.requireCreateIdentityOttEnrollmentUnfinished(eid.New(), false) + ctx.Req.NoError(ctx.waitForIdentityOnPeer(0, identityId, 5*time.Second)) + certAuth := ctx.completeOttEnrollmentAtApiHost(identityId, ctx.PeerControllerApiHosts()[0]) + + // The peer controller signs with its own intermediate under the shared signing root. + ctx.Req.Equal("Controller Two Edge Signing Cert", certAuth.certs[0].Issuer.CommonName) + + ctx.Req.NoError(dialServiceWithFirstPartyCert(ctx, testSigner, identityId, certAuth, testService.Name)) + }) + + t.Run("controller store records hold full cert chains", func(t *testing.T) { + ctx.NextTest(t) + + result, err := ctx.EdgeController.AppEnv.Managers.Controller.BaseList("true limit none") + ctx.Req.NoError(err) + ctx.Req.Len(result.Entities, 3) + + for _, controllerEntity := range result.Entities { + certs := nfpem.PemStringToCertificates(controllerEntity.CertPem) + ctx.Req.GreaterOrEqual(len(certs), 2, "controller %s certPem should hold the full chain", controllerEntity.Id) + } + }) +} diff --git a/tests/request_body_limit_test.go b/tests/request_body_limit_test.go new file mode 100644 index 000000000..e65e3a4e9 --- /dev/null +++ b/tests/request_body_limit_test.go @@ -0,0 +1,98 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "bytes" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/openziti/ziti/v2/controller/api" +) + +// Test_RequestBodyLimit locks in the pre-auth request body cap. The controller web APIs +// buffer each request body into memory before any authentication check, so bodies larger +// than api.MaxRequestBodySize must be rejected with 413 instead of being buffered. +func Test_RequestBodyLimit(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + + oversized := bytes.Repeat([]byte("a"), api.MaxRequestBodySize+1) + + t.Run("oversized body to the unauthenticated client API enroll endpoint returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.newAnonymousClientApiRequest().SetBody(oversized).Post("/enroll") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized body must be rejected before it is buffered, got body: %s", resp.String()) + }) + + t.Run("oversized body to the unauthenticated management API authenticate endpoint returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.newAnonymousManagementApiRequest().SetBody(oversized).Post("/authenticate?method=password") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized body must be rejected before it is buffered, got body: %s", resp.String()) + }) + + t.Run("oversized chunked body without a Content-Length header returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + // a reader body makes resty send chunked transfer encoding, so the server cannot + // reject on the Content-Length header and must stop reading at the cap instead + resp, err := ctx.newAnonymousClientApiRequest().SetBody(bytes.NewReader(oversized)).Post("/enroll") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized chunked body must be rejected once the cap is hit, got body: %s", resp.String()) + }) + + t.Run("oversized body to the unauthenticated OIDC login endpoint returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + client := resty.NewWithClient(ctx.NewHttpClient(ctx.NewTransport())) + + resp, err := client.R(). + SetHeader("content-type", "application/json"). + SetBody(oversized). + Post("https://" + ctx.ApiHost + "/oidc/login/username") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized body must be rejected before it is buffered, got body: %s", resp.String()) + }) + + t.Run("normal-size body still reaches the API handlers", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.newAnonymousManagementApiRequest(). + SetBody(`{"username":"bogus","password":"bogus"}`). + Post("/authenticate?method=password") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusUnauthorized, resp.StatusCode(), + "a small body must pass the cap and reach normal authentication handling") + }) +} diff --git a/tests/revocation_enforcement_oidc_test.go b/tests/revocation_enforcement_oidc_test.go index 070afeb37..08bde55c7 100644 --- a/tests/revocation_enforcement_oidc_test.go +++ b/tests/revocation_enforcement_oidc_test.go @@ -140,6 +140,18 @@ func echoServerHandler(conn *testServerConn) error { } } +// waitForSecondAfter blocks until the wall clock reaches a whole second later than the one containing +// ts, returning immediately when it already has. Call it before authenticating a session that has to +// be observably newer than a revocation created at ts: a token's iat claim carries whole seconds, so +// enforcement cannot tell a session issued just before a revocation from one issued just after it +// within the same second, and revokes both. +func waitForSecondAfter(ts time.Time) { + next := ts.Truncate(time.Second).Add(time.Second) + if remaining := time.Until(next); remaining > 0 { + time.Sleep(remaining + 10*time.Millisecond) + } +} + // freshAuthedContext creates and authenticates a new OIDC SDK context for an // existing identity, yielding a brand-new api-session (a fresh z_asid and issue // time). Authenticate on an already-authenticated context is a no-op, so a new @@ -258,6 +270,7 @@ func Test_Revocation_IdentityCutoff_PostCutoffSessionSurvives_OIDC(t *testing.T) // can still authenticate a new session below.) _, err := adminApi.CreateRevocation(dialIdentity.Id, rest_model.RevocationTypeEnumIDENTITY) ctx.Req.NoError(err) + revocationCreated := time.Now() requireConnClosedByReaper(ctx, clientConn) t.Run("a session authenticated after the cutoff survives", func(t *testing.T) { @@ -265,6 +278,7 @@ func Test_Revocation_IdentityCutoff_PostCutoffSessionSurvives_OIDC(t *testing.T) // A fresh session is issued after the cutoff, so it must not be revoked // even though the identity revocation still lingers. + waitForSecondAfter(revocationCreated) freshContext := freshAuthedContext(t, ctx, dialIdentity) newConn := ctx.WrapConn(freshContext.Dial(svc.Name)) requireConnSurvivesReaper(ctx, newConn) diff --git a/tests/session_legacy_token_id_test.go b/tests/session_legacy_token_id_test.go new file mode 100644 index 000000000..f448493c3 --- /dev/null +++ b/tests/session_legacy_token_id_test.go @@ -0,0 +1,108 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/Jeffail/gabs" + "github.com/openziti/ziti/v2/common/eid" +) + +// jtiFromServiceToken returns the "jti" (session id) claim from a service-access JWT without +// verifying its signature. +func jtiFromServiceToken(t *testing.T, token string) string { + parts := strings.Split(token, ".") + if len(parts) != 3 { + t.Fatalf("expected a 3-segment JWT, got %d segments", len(parts)) + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatalf("failed to base64-decode JWT payload: %v", err) + } + + claims := map[string]any{} + if err := json.Unmarshal(payload, &claims); err != nil { + t.Fatalf("failed to parse JWT payload: %v", err) + } + + jti, _ := claims["jti"].(string) + return jti +} + +// Test_Legacy_Session_Token_Matches_Persisted_Session guards the legacy create-session flow +// against a regression where the JWT was minted with a freshly generated session id before the +// durable Create ran. When a session already existed for the (api-session, type, service), Create +// deduped the entity to the existing id, but the token had already committed to the new id, so the +// client held a token whose id backed no stored session. Every subsequent create-circuit / +// create-terminator then failed to load the session. The token's id must always match the durable +// session's id, including when a create dedups to an existing session. +func Test_Legacy_Session_Token_Matches_Persisted_Session(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + ctx.CreateEnrollAndStartEdgeRouter() + + identityRole := eid.New() + serviceRole := eid.New() + + _, identityAuth := ctx.AdminManagementSession.requireCreateIdentityOttEnrollment(eid.New(), false, identityRole) + clientSession, err := identityAuth.AuthenticateClientApi(ctx) + ctx.Req.NoError(err) + + service := ctx.AdminManagementSession.requireNewService(s(serviceRole), nil) + ctx.AdminManagementSession.requireNewServicePolicy("Dial", s("#"+serviceRole), s("#"+identityRole), nil) + ctx.AdminManagementSession.requireNewEdgeRouterPolicy(s("#all"), s("#"+identityRole)) + ctx.AdminManagementSession.requireNewServiceEdgeRouterPolicy(s("#all"), s("#"+serviceRole)) + + // first create persists a new durable session; its id and token must agree + resp, err := clientSession.createNewSession(service.Id) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) + + first, err := gabs.ParseJSON(resp.Body()) + ctx.Req.NoError(err) + firstId, ok := first.Path("data.id").Data().(string) + ctx.Req.True(ok, "first response is missing data.id") + firstToken, ok := first.Path("data.token").Data().(string) + ctx.Req.True(ok, "first response is missing data.token") + ctx.Req.Equal(firstId, jtiFromServiceToken(t, firstToken), "token id must match session id on first create") + + // a second create for the same (api-session, type, service) dedups to the existing session; the + // returned token must still reference that persisted session id, not a freshly generated one + resp, err = clientSession.createNewSession(service.Id) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) + + second, err := gabs.ParseJSON(resp.Body()) + ctx.Req.NoError(err) + secondId, ok := second.Path("data.id").Data().(string) + ctx.Req.True(ok, "second response is missing data.id") + secondToken, ok := second.Path("data.token").Data().(string) + ctx.Req.True(ok, "second response is missing data.token") + + ctx.Req.Equal(firstId, secondId, "dedup must return the existing session id") + ctx.Req.Equal(secondId, jtiFromServiceToken(t, secondToken), "token id must match the persisted (deduped) session id") +} diff --git a/tests/terminator_ownership_test.go b/tests/terminator_ownership_test.go new file mode 100644 index 000000000..4addf719c --- /dev/null +++ b/tests/terminator_ownership_test.go @@ -0,0 +1,131 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/pb/ctrl_pb" + "github.com/openziti/ziti/v2/controller/xt_smartrouting" + "google.golang.org/protobuf/proto" +) + +// Test_TerminatorOwnership drives the fabric control channel directly to confirm that terminator +// operations are scoped to the requesting router: a router may only remove or update terminators it +// owns. Without that scoping any enrolled router can delete another router's terminators (taking +// down services hosted elsewhere) or re-weight them to steer traffic. +func Test_TerminatorOwnership(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + svc := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll(xt_smartrouting.Name) + + // The requesting router: real, enrolled, and connected, so we can send on its control channel. + requestingRouter := ctx.CreateEnrollAndStartEdgeRouter() + + // The victim router only needs to exist in the model to own a terminator; it is never started. + victimRouter := ctx.AdminManagementSession.requireNewEdgeRouter() + + ctrlCh := requestingRouter.GetNetworkControllers().AnyCtrlChannel() + ctx.Req.NotNil(ctrlCh) + + // sendForResult sends a fabric ctrl_pb request and returns the controller's Result. + sendForResult := func(contentType int32, body proto.Message) *channel.Result { + bodyBytes, err := proto.Marshal(body) + ctx.Req.NoError(err) + + msg := channel.NewMessage(contentType, bodyBytes) + reply, err := msg.WithTimeout(5 * time.Second).SendForReply(ctrlCh.GetDefaultSender()) + ctx.Req.NoError(err) + + return channel.UnmarshalResult(reply) + } + + terminatorExists := func(id string) bool { + return ctx.AdminManagementSession.requireQuery("terminators/"+id) != nil + } + + terminatorPrecedence := func(id string) string { + entity := ctx.AdminManagementSession.requireQuery("terminators/" + id) + return entity.Path("data.precedence").Data().(string) + } + + newVictimTerminator := func() string { + term := ctx.AdminManagementSession.requireNewTerminator(svc.Id, victimRouter.id, "transport", "tcp:localhost:1234") + return term.id + } + + t.Run("cannot remove a terminator owned by another router", func(t *testing.T) { + ctx.NextTest(t) + + terminatorId := newVictimTerminator() + + result := sendForResult(int32(ctrl_pb.ContentType_RemoveTerminatorRequestType), + &ctrl_pb.RemoveTerminatorRequest{TerminatorId: terminatorId}) + + ctx.Req.False(result.Success, "removing another router's terminator must be rejected") + ctx.Req.True(terminatorExists(terminatorId), "the victim's terminator must survive the rejected removal") + }) + + t.Run("cannot remove another router's terminator in a batch", func(t *testing.T) { + ctx.NextTest(t) + + terminatorId := newVictimTerminator() + + // The batch handler drops ids it does not own rather than failing the whole request, so the + // assertion that matters is that the terminator survives. + sendForResult(int32(ctrl_pb.ContentType_RemoveTerminatorsRequestType), + &ctrl_pb.RemoveTerminatorsRequest{TerminatorIds: []string{terminatorId}}) + + ctx.Req.True(terminatorExists(terminatorId), "the victim's terminator must survive the batch removal") + }) + + t.Run("cannot re-weight a terminator owned by another router", func(t *testing.T) { + ctx.NextTest(t) + + terminatorId := newVictimTerminator() + ctx.Req.Equal("default", terminatorPrecedence(terminatorId)) + + result := sendForResult(int32(ctrl_pb.ContentType_UpdateTerminatorRequestType), + &ctrl_pb.UpdateTerminatorRequest{ + TerminatorId: terminatorId, + UpdatePrecedence: true, + Precedence: ctrl_pb.TerminatorPrecedence_Failed, + }) + + ctx.Req.False(result.Success, "updating another router's terminator must be rejected") + ctx.Req.Equal("default", terminatorPrecedence(terminatorId), + "the victim's terminator precedence must be unchanged") + }) + + t.Run("can remove a terminator it owns", func(t *testing.T) { + ctx.NextTest(t) + + term := ctx.AdminManagementSession.requireNewTerminator(svc.Id, requestingRouter.GetRouterId().Token, "transport", "tcp:localhost:1234") + + result := sendForResult(int32(ctrl_pb.ContentType_RemoveTerminatorRequestType), + &ctrl_pb.RemoveTerminatorRequest{TerminatorId: term.id}) + + ctx.Req.True(result.Success, "a router must still be able to remove its own terminator: %v", result.Message) + }) +} diff --git a/tests/testdata/configs/README.md b/tests/testdata/configs/README.md index fa7abaec5..6884d1d81 100644 --- a/tests/testdata/configs/README.md +++ b/tests/testdata/configs/README.md @@ -15,6 +15,38 @@ All paths inside config files are relative to the `tests/` working directory (th standard Go test working directory for this package), so cert and key paths such as `testdata/ca/...` resolve correctly regardless of which config set is active. +## Test PKI + +The raft/HA config sets use a SPIFFE-capable PKI under `testdata/pki/`, generated by +`tests/testdata/create-pki.sh` (and the equivalent `create-pki.ps1`) with `ziti pki`. +The other config sets continue to use the older `testdata/ca` material. + +- A single root CA, `Ziti Test Root CA`, under `pki/root/`. +- Three controller intermediates (`ctrl1`, `ctrl2`, `ctrl3`), each signing its own + server and client certs with a `spiffe://ziti.test/controller/` URI SAN. +- Fabric routers `001` and `002`, signed by the `ctrl1` intermediate, each with a + shared key backing its server and client certs and a `spiffe://ziti.test/router/` + URI SAN. +- A `ctrl1` wildcard alt server cert (`ctrl1-wildcard.chain.pem` / `ctrl1-wildcard.key`) + whose only SAN is `*.wildcard.test`. +- A separate edge signing PKI: a second self-signed root, `Ziti Test Edge Signing Root CA`, + under `pki/signing-root/`, with per-controller signing intermediates (`signing1`, `signing2`, + `signing3`) and a shared bundle (`pki/signing-root/certs/signing-bundle.pem`, root + all three + intermediates). Used by the `ha-3` config set to model networks whose + `edge.enrollment.signingCert` root differs from the ctrl-channel root CA. + +In the identity blocks of configs using this PKI: + +- `cert` / `server_cert` point at the full chain files (`*.chain.pem`, leaf → intermediate + → root) so peers can build the chain from what is presented on the wire. +- `ca` is the root cert only (`testdata/pki/root/certs/root.cert`); the trust anchor is the + root, and intermediates arrive in the presented chains. +- the controller's client and server certs use separate keys, so `key` and `server_key` + are both set. + +To regenerate the PKI, run `create-pki.sh` (or `create-pki.ps1`) from anywhere; it anchors +itself to `tests/testdata/` and writes to `pki/`. + ## Directory Layout ``` @@ -51,6 +83,16 @@ testdata/configs/ `alt_server_certs` entry whose only SAN is the wildcard `*.wildcard.test`, with the `edge-oidc` binding setting `allowedHostnames: [ctrl.wildcard.test]`. Used by `Test_OidcDiscoveryEndpoints_WildcardIssuer`. +- **`ha-3`** (`Ha3`) — Three-controller raft cluster whose edge signing CA root + (`pki/signing-root`) is distinct from the ctrl-channel root CA (`pki/root`). Each controller + signs identity certs with its own intermediate under the shared signing root + (`signing1/2/3`), and all three point `edge.enrollment.signingCert.ca` at the shared signing + bundle. Controllers listen on 1281/1282/1283 (web), 6262/6363/6464 (ctrl), 10000/10001/10002 + (mgmt); raft data lives under `testdata/ha-3-data/ctrl{1,2,3}`. Includes an edge router config + listing all three ctrl endpoints. Started with `StartHaCluster`. Used to exercise first-party + client cert validation when the signing CA and ctrl-channel CA differ, including certs issued + by a controller other than the one a router is subscribed to. + ## Adding a New Config Set 1. Create a subdirectory with a short, descriptive name (kebab-case). diff --git a/tests/testdata/configs/ha-3/ctrl1.yml b/tests/testdata/configs/ha-3/ctrl1.yml new file mode 100644 index 000000000..72bc7b173 --- /dev/null +++ b/tests/testdata/configs/ha-3/ctrl1.yml @@ -0,0 +1,60 @@ +v: 3 + +# Controller 1 of the ha-3 config set: a three-controller raft cluster whose edge signing CA +# root (pki/signing-root) is distinct from the ctrl-channel root CA (pki/root). Each controller +# signs identity certs with its own intermediate under the shared signing root. The dataDir is +# cleaned by StartHaCluster before each run. +cluster: + dataDir: testdata/ha-3-data/ctrl1 + +identity: + cert: testdata/pki/ctrl1/certs/client.chain.pem + server_cert: testdata/pki/ctrl1/certs/server.chain.pem + key: testdata/pki/ctrl1/keys/client.key + server_key: testdata/pki/ctrl1/keys/server.key + ca: testdata/pki/root/certs/root.cert + +trustDomain: ziti.test + +ctrl: + listener: tls:127.0.0.1:6262 + options: + advertiseAddress: tls:127.0.0.1:6262 + +mgmt: + listener: tls:127.0.0.1:10000 + +terminator: + validators: + edge: edge + +edge: + api: + sessionTimeout: 30m + address: 127.0.0.1:1281 + enrollment: + signingCert: + cert: testdata/pki/signing1/certs/signing1.chain.pem + key: testdata/pki/signing1/keys/signing1.key + ca: testdata/pki/signing-root/certs/signing-bundle.pem + edgeIdentity: + duration: 20m + edgeRouter: + duration: 60m + +web: + - name: client-management-localhost + bindPoints: + - interface: 127.0.0.1:1281 + address: 127.0.0.1:1281 + options: {} + apis: + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc + options: + redirectURIs: + - "http://localhost:*/auth/callback" + - "http://127.0.0.1:*/auth/callback" diff --git a/tests/testdata/configs/ha-3/ctrl2.yml b/tests/testdata/configs/ha-3/ctrl2.yml new file mode 100644 index 000000000..ea4bd03fd --- /dev/null +++ b/tests/testdata/configs/ha-3/ctrl2.yml @@ -0,0 +1,57 @@ +v: 3 + +# Controller 2 of the ha-3 config set. See ctrl1.yml for the set's description. +cluster: + dataDir: testdata/ha-3-data/ctrl2 + +identity: + cert: testdata/pki/ctrl2/certs/client.chain.pem + server_cert: testdata/pki/ctrl2/certs/server.chain.pem + key: testdata/pki/ctrl2/keys/client.key + server_key: testdata/pki/ctrl2/keys/server.key + ca: testdata/pki/root/certs/root.cert + +trustDomain: ziti.test + +ctrl: + listener: tls:127.0.0.1:6363 + options: + advertiseAddress: tls:127.0.0.1:6363 + +mgmt: + listener: tls:127.0.0.1:10001 + +terminator: + validators: + edge: edge + +edge: + api: + sessionTimeout: 30m + address: 127.0.0.1:1282 + enrollment: + signingCert: + cert: testdata/pki/signing2/certs/signing2.chain.pem + key: testdata/pki/signing2/keys/signing2.key + ca: testdata/pki/signing-root/certs/signing-bundle.pem + edgeIdentity: + duration: 20m + edgeRouter: + duration: 60m + +web: + - name: client-management-localhost + bindPoints: + - interface: 127.0.0.1:1282 + address: 127.0.0.1:1282 + options: {} + apis: + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc + options: + redirectURIs: + - "http://localhost:*/auth/callback" + - "http://127.0.0.1:*/auth/callback" diff --git a/tests/testdata/configs/ha-3/ctrl3.yml b/tests/testdata/configs/ha-3/ctrl3.yml new file mode 100644 index 000000000..81a5c4f88 --- /dev/null +++ b/tests/testdata/configs/ha-3/ctrl3.yml @@ -0,0 +1,57 @@ +v: 3 + +# Controller 3 of the ha-3 config set. See ctrl1.yml for the set's description. +cluster: + dataDir: testdata/ha-3-data/ctrl3 + +identity: + cert: testdata/pki/ctrl3/certs/client.chain.pem + server_cert: testdata/pki/ctrl3/certs/server.chain.pem + key: testdata/pki/ctrl3/keys/client.key + server_key: testdata/pki/ctrl3/keys/server.key + ca: testdata/pki/root/certs/root.cert + +trustDomain: ziti.test + +ctrl: + listener: tls:127.0.0.1:6464 + options: + advertiseAddress: tls:127.0.0.1:6464 + +mgmt: + listener: tls:127.0.0.1:10002 + +terminator: + validators: + edge: edge + +edge: + api: + sessionTimeout: 30m + address: 127.0.0.1:1283 + enrollment: + signingCert: + cert: testdata/pki/signing3/certs/signing3.chain.pem + key: testdata/pki/signing3/keys/signing3.key + ca: testdata/pki/signing-root/certs/signing-bundle.pem + edgeIdentity: + duration: 20m + edgeRouter: + duration: 60m + +web: + - name: client-management-localhost + bindPoints: + - interface: 127.0.0.1:1283 + address: 127.0.0.1:1283 + options: {} + apis: + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc + options: + redirectURIs: + - "http://localhost:*/auth/callback" + - "http://127.0.0.1:*/auth/callback" diff --git a/tests/testdata/configs/ha-3/edge-router.yml b/tests/testdata/configs/ha-3/edge-router.yml new file mode 100644 index 000000000..4d7f30782 --- /dev/null +++ b/tests/testdata/configs/ha-3/edge-router.yml @@ -0,0 +1,40 @@ +v: 3 + +# Edge router for the ha-3 config set. The identity files are written by enrollment at test +# runtime; the names are distinct from the default-ats router so the sets don't clobber each +# other. All three controller ctrl endpoints are listed. +identity: + cert: testdata/edge-router/ha-3-edge-router-client.cert.pem + server_cert: testdata/edge-router/ha-3-edge-router-server.cert.pem + key: testdata/edge-router/ha-3-edge-router.key.pem + ca: testdata/edge-router/ha-3-edge-router-ca-chain.cert.pem + +ctrl: + endpoints: + - tls:127.0.0.1:6262 + - tls:127.0.0.1:6363 + - tls:127.0.0.1:6464 + +edge: + heartbeatIntervalSeconds: 10 + csr: + country: US + province: NC + locality: Charlotte + organization: NetFoundry + organizationalUnit: Ziti + sans: + dns: + - "localhost" + ip: + - "127.0.0.1" + +dialers: + - binding: udp + - binding: transport + +listeners: + - binding: edge + address: tls:0.0.0.0:3022 + options: + advertise: 127.0.0.1:3022 diff --git a/tests/testdata/create-pki.ps1 b/tests/testdata/create-pki.ps1 new file mode 100644 index 000000000..c587ca0be --- /dev/null +++ b/tests/testdata/create-pki.ps1 @@ -0,0 +1,94 @@ +# Builds a fresh, HA/SPIFFE-capable test PKI under testdata/pki, modeled on doc/ha/create-pki.sh. +# Separate from the existing testdata/ca PKI, which is left untouched. Can be run from any +# directory; it anchors itself to the script's own location. + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +# Anchor to the script's own directory (tests/testdata) so paths resolve regardless of cwd. +Set-Location -LiteralPath $PSScriptRoot + +$PkiRoot = 'pki' +$TrustDomain = 'ziti.test' +$RootNotBefore = (Get-Date).ToUniversalTime().AddHours(-24).ToString('yyyy-MM-ddTHH:mm:ss') + 'Z' +$IntermediateNotBefore = (Get-Date).ToUniversalTime().AddHours(-12).ToString('yyyy-MM-ddTHH:mm:ss') + 'Z' + +# Self-signed root CA. --trust-domain makes every cert issued under it SPIFFE-capable. +ziti pki create ca --pki-root $PkiRoot --trust-domain $TrustDomain --ca-file root --ca-name 'Ziti Test Root CA' --not-before $RootNotBefore + +# Controller 1: intermediate (signing) cert +ziti pki create intermediate --pki-root $PkiRoot --ca-name root --intermediate-file ctrl1 --intermediate-name 'Controller One Signing Cert' --not-before $IntermediateNotBefore + +# Controller 1: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --dns localhost --ip 127.0.0.1 --server-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl1 --client-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: wildcard alt server cert (only SAN is *.wildcard.test). Used by the +# wildcard-oidc-server config set / Test_OidcDiscoveryEndpoints_WildcardIssuer to exercise +# OIDC issuer derivation from a wildcard server-cert SAN. +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --server-file ctrl1-wildcard --server-name ctrl1-wildcard --dns '*.wildcard.test' + +# Controller 2: intermediate (signing) cert +ziti pki create intermediate --pki-root $PkiRoot --ca-name root --intermediate-file ctrl2 --intermediate-name 'Controller Two Signing Cert' --not-before $IntermediateNotBefore + +# Controller 2: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl2 --dns localhost --ip 127.0.0.1 --server-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 2: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl2 --client-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 3: intermediate (signing) cert +ziti pki create intermediate --pki-root $PkiRoot --ca-name root --intermediate-file ctrl3 --intermediate-name 'Controller Three Signing Cert' --not-before $IntermediateNotBefore + +# Controller 3: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl3 --dns localhost --ip 127.0.0.1 --server-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Controller 3: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl3 --client-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Routers are signed by controller 1's intermediate. Each router uses a single key +# shared by its server cert (link/edge listeners) and client cert (ctrl channel). + +# Router 001: key +ziti pki create key --pki-root $PkiRoot --ca-name ctrl1 --key-file 001 + +# Router 001: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --key-file 001 --server-file 001-server --server-name 001 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/001' + +# Router 001: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl1 --key-file 001 --client-file 001-client --client-name 001 --spiffe-id 'router/001' + +# Router 002: key +ziti pki create key --pki-root $PkiRoot --ca-name ctrl1 --key-file 002 + +# Router 002: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --key-file 002 --server-file 002-server --server-name 002 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/002' + +# Router 002: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl1 --key-file 002 --client-file 002-client --client-name 002 --spiffe-id 'router/002' + +# Separate edge signing PKI: a self-signed root distinct from the ctrl-channel root, with +# per-controller signing intermediates. Used by the ha-3 config set to exercise networks whose +# edge.enrollment.signingCert root differs from the ctrl-channel root CA. +ziti pki create ca --pki-root $PkiRoot --trust-domain $TrustDomain --ca-file signing-root --ca-name 'Ziti Test Edge Signing Root CA' --not-before $RootNotBefore + +# Controller 1: edge signing intermediate +ziti pki create intermediate --pki-root $PkiRoot --ca-name signing-root --intermediate-file signing1 --intermediate-name 'Controller One Edge Signing Cert' --not-before $IntermediateNotBefore + +# Controller 2: edge signing intermediate +ziti pki create intermediate --pki-root $PkiRoot --ca-name signing-root --intermediate-file signing2 --intermediate-name 'Controller Two Edge Signing Cert' --not-before $IntermediateNotBefore + +# Controller 3: edge signing intermediate +ziti pki create intermediate --pki-root $PkiRoot --ca-name signing-root --intermediate-file signing3 --intermediate-name 'Controller Three Edge Signing Cert' --not-before $IntermediateNotBefore + +# Shared edge signing CA bundle: the signing root plus every per-controller signing +# intermediate. Each controller's edge.enrollment.signingCert.ca points here, so the +# controllers publish the signing root as a trust anchor with the intermediates attached. +Get-Content ` + "$PkiRoot/signing-root/certs/signing-root.cert", ` + "$PkiRoot/signing1/certs/signing1.cert", ` + "$PkiRoot/signing2/certs/signing2.cert", ` + "$PkiRoot/signing3/certs/signing3.cert" | + Set-Content "$PkiRoot/signing-root/certs/signing-bundle.pem" diff --git a/tests/testdata/create-pki.sh b/tests/testdata/create-pki.sh new file mode 100644 index 000000000..a50f98236 --- /dev/null +++ b/tests/testdata/create-pki.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# Builds a fresh, HA/SPIFFE-capable test PKI under testdata/pki, modeled on doc/ha/create-pki.sh. +# Separate from the existing testdata/ca PKI, which is left untouched. Can be run from any +# directory; it anchors itself to the script's own location. + +set -euo pipefail + +# Anchor to the script's own directory (tests/testdata) so paths resolve regardless of cwd. +cd "$(dirname "${BASH_SOURCE[0]}")" + +PKI_ROOT="pki" +TRUST_DOMAIN="ziti.test" +# Epoch math (-d @SECONDS) so this works with both GNU date and BusyBox date. +NOW="$(date -u +%s)" +ROOT_NOT_BEFORE="$(date -u -d "@$((NOW - 86400))" +%Y-%m-%dT%H:%M:%SZ)" # 24h before now +INTERMEDIATE_NOT_BEFORE="$(date -u -d "@$((NOW - 43200))" +%Y-%m-%dT%H:%M:%SZ)" # 12h before now + +# Self-signed root CA. --trust-domain makes every cert issued under it SPIFFE-capable. +ziti pki create ca \ + --pki-root "$PKI_ROOT" \ + --trust-domain "$TRUST_DOMAIN" \ + --ca-file root \ + --ca-name 'Ziti Test Root CA' \ + --not-before "$ROOT_NOT_BEFORE" + +# Controller 1: intermediate (signing) cert +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name root --intermediate-file ctrl1 --intermediate-name 'Controller One Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 1: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --dns localhost --ip 127.0.0.1 --server-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl1 --client-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: wildcard alt server cert (only SAN is *.wildcard.test). Used by the +# wildcard-oidc-server config set / Test_OidcDiscoveryEndpoints_WildcardIssuer to exercise +# OIDC issuer derivation from a wildcard server-cert SAN. +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --server-file ctrl1-wildcard --server-name ctrl1-wildcard --dns '*.wildcard.test' + +# Controller 2: intermediate (signing) cert +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name root --intermediate-file ctrl2 --intermediate-name 'Controller Two Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 2: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl2 --dns localhost --ip 127.0.0.1 --server-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 2: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl2 --client-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 3: intermediate (signing) cert +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name root --intermediate-file ctrl3 --intermediate-name 'Controller Three Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 3: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl3 --dns localhost --ip 127.0.0.1 --server-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Controller 3: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl3 --client-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Routers are signed by controller 1's intermediate. Each router uses a single key +# shared by its server cert (link/edge listeners) and client cert (ctrl channel). + +# Router 001: key +ziti pki create key --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 001 + +# Router 001: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 001 --server-file 001-server --server-name 001 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/001' + +# Router 001: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 001 --client-file 001-client --client-name 001 --spiffe-id 'router/001' + +# Router 002: key +ziti pki create key --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 002 + +# Router 002: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 002 --server-file 002-server --server-name 002 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/002' + +# Router 002: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 002 --client-file 002-client --client-name 002 --spiffe-id 'router/002' + +# Separate edge signing PKI: a self-signed root distinct from the ctrl-channel root, with +# per-controller signing intermediates. Used by the ha-3 config set to exercise networks whose +# edge.enrollment.signingCert root differs from the ctrl-channel root CA. +ziti pki create ca \ + --pki-root "$PKI_ROOT" \ + --trust-domain "$TRUST_DOMAIN" \ + --ca-file signing-root \ + --ca-name 'Ziti Test Edge Signing Root CA' \ + --not-before "$ROOT_NOT_BEFORE" + +# Controller 1: edge signing intermediate +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name signing-root --intermediate-file signing1 --intermediate-name 'Controller One Edge Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 2: edge signing intermediate +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name signing-root --intermediate-file signing2 --intermediate-name 'Controller Two Edge Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 3: edge signing intermediate +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name signing-root --intermediate-file signing3 --intermediate-name 'Controller Three Edge Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Shared edge signing CA bundle: the signing root plus every per-controller signing +# intermediate. Each controller's edge.enrollment.signingCert.ca points here, so the +# controllers publish the signing root as a trust anchor with the intermediates attached. +cat "$PKI_ROOT/signing-root/certs/signing-root.cert" \ + "$PKI_ROOT/signing1/certs/signing1.cert" \ + "$PKI_ROOT/signing2/certs/signing2.cert" \ + "$PKI_ROOT/signing3/certs/signing3.cert" \ + > "$PKI_ROOT/signing-root/certs/signing-bundle.pem" diff --git a/tests/testdata/pki/ctrl1/certs/001-client.cert b/tests/testdata/pki/ctrl1/certs/001-client.cert new file mode 100644 index 000000000..8d9a12d70 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF2zCCA8OgAwIBAgIRAPTDin3ED3ygY+iJjALR770wDQYJKoZIhvcNAQELBQAw +bjELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEkMCIGA1UEAxMbQ29udHJvbGxlciBP +bmUgU2lnbmluZyBDZXJ0MB4XDTI2MDYwODE5NDYwM1oXDTI3MDYwODE5NDcwM1ow +VjELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEMMAoGA1UEAxMDMDAxMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxgJ59dIOqiW43b+1kzC3vTwVKKcNDtCC +nf7PCrge5SV4p2EuPzilmub7jGBwr3VE7ONyEzGE9UgAANzR4cWxu7KIbUwXYpSg +eik4gScmpdVk/K9EpFbUA7qS/H/ap3nhgRVksYTmfBoe1YruD+mbzFGs1K4htKCL +f8HTb60fL8I6mLh0mLUh/nSZfhugFqtqA5S17o6fa2JgXr7LchJudV2qm7PSN4N/ +F1rZsnG9aaVqb00/0NGGawA5tY/Cj3WY9e+524nlchosVmw9ZqRTUXaG0qvAhecy ++NhAI9B7heVMiN+tpCWr6dUN3DQnGcStlrr6TPrKiw00Y8Km9ML5QtD11yWrR4uw +018nERCCn1tyWa0vXtMXpYmcWdzfXjeitUkSjXRbObiD05QdmmFFBqXq/teMembN +7D9vIgn/7KFftXIrnOXNnihbybaDrNdx9AuUBBxd3gUBRsYX78mCfYFih0HfGas3 +DGShpcfACeX9tgSUw1GMfW7HQu6JBBxr8YgWWSNNEvw4SdLmf7KtjIC7PjXqfbve +Vy7tiBDcyRh390zwtWC3mekZug8+wvTgzJNItg7m15cU0558aos6j0mMesLGOZoQ +ZMr2ysCXnhAhcnFx4ObnUg0TDwIw9zd/ByzEAuR6T3fj8hKi8A2R57wO03xCTZ0t +K+ja/vaAPCsCAwEAAaOBizCBiDAOBgNVHQ8BAf8EBAMCBeAwDAYDVR0TAQH/BAIw +ADAdBgNVHQ4EFgQUidhPZduePGRw8OAjIlWwZSYDFGowHwYDVR0jBBgwFoAUsAfF +d75WNXU0nyoEoxU0q1h+vUgwKAYDVR0RBCEwH4Ydc3BpZmZlOi8veml0aS50ZXN0 +L3JvdXRlci8wMDEwDQYJKoZIhvcNAQELBQADggIBAHEQtAadf1FzD7jkGnKVOPRp +mBkooTxw+wULF752A9sMpLQ6pZRg1x58M3/hNSt3dVG5D85Pl5rKSVD08PCsdFez +9v/ofiCXJtim+QHk2GOC1GXgtbFdPMPNAwQnrNOOuH9UwZBRbReZKzi198O3vQ4b +RvJwa3524KBS9yGhvyxTTr4kVWDr8VHT6mnnI6teRdDq0X/iJs5E9DiqUMWclEAO +I75OYjFbfzlYhWDwbqBz6mB6ZoUyV3jG/xlJRWreMV2IMkz5j+nnCfwFF6C2p8ak +jbdwQLlyrCZ/HeaCnrNGS4uGJgpVuETjkGgOsheGThRxeJ/9J/OSQ6R3KtZN7E8N +bToR7PdkkP+h6kp4YX24e6QroK72+l0t9zf7IBFbR7z+z6cB6skZpzslRx+W7SnH +rYqiitIwcwR1ookldIrO44dLtlYjaoD0JhRI1oDkKVQ4flWQZCScuDRMsU6AuOn6 +u0q/zA04jkmSoabd/0IIurV/D31ip6XVdwHbUYdzGIneM8cdwSc5v4jLlIyDVDPC +qnOgPx31VKNB5hE5T3ds5Z9Bhxz7yABUct+ts2sx6F6F/hSXu9vvIAt2sAyRYGSb +no2au0awmKWkm5G0/SDOsx6CfDGIoYg+rR/8N7oFzOmSzpd7LebW9IB5zcBRfd8V +ZI+UH00lzkWUe9rGcWXA +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/001-client.chain.pem b/tests/testdata/pki/ctrl1/certs/001-client.chain.pem new file mode 100644 index 000000000..67920b991 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +ZI+UH00lzkWUe9rGcWXA +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/001-server.cert b/tests/testdata/pki/ctrl1/certs/001-server.cert new file mode 100644 index 000000000..9031ed53c --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/001-server.chain.pem b/tests/testdata/pki/ctrl1/certs/001-server.chain.pem new file mode 100644 index 000000000..469d87713 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-client.cert b/tests/testdata/pki/ctrl1/certs/002-client.cert new file mode 100644 index 000000000..58a737326 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +lCamByWqrRsC9gOONRg= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-client.chain.pem b/tests/testdata/pki/ctrl1/certs/002-client.chain.pem new file mode 100644 index 000000000..c6af2c6f6 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +lCamByWqrRsC9gOONRg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-server.cert b/tests/testdata/pki/ctrl1/certs/002-server.cert new file mode 100644 index 000000000..3c6d47f1c --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-server.chain.pem b/tests/testdata/pki/ctrl1/certs/002-server.chain.pem new file mode 100644 index 000000000..f8c8d2d49 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/client.cert b/tests/testdata/pki/ctrl1/certs/client.cert new file mode 100644 index 000000000..d5e9e5a8e --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +6WGKTA7mi2TXOGLv7velocFTYMO8fQ== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/client.chain.pem b/tests/testdata/pki/ctrl1/certs/client.chain.pem new file mode 100644 index 000000000..7c6b1abaa --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +6WGKTA7mi2TXOGLv7velocFTYMO8fQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert new file mode 100644 index 000000000..abc5e70e2 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF1TCCA72gAwIBAgIQDMbvu6na8/uUMbRL+PYXajANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIE9u +ZSBTaWduaW5nIENlcnQwHhcNMjYwNjE1MTczNDM4WhcNMjcwNjE1MTczNTM4WjBh +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMRcwFQYDVQQDEw5jdHJsMS13aWxkY2Fy +ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALdz2IJ9kZV42nBIIyKN +xTNnVYSNj3Y6NgP4dKiw9MvpeomAJ46Z5SBMcjP65huKxUS7+i6BGROdzaFcVzt7 +ZHVhLCs5K7/XCDPA18klbciIU8zfZKbS2FD5J5gA6xZWey8apButysnFpXlqquhX +dZPCNRex9Dllnc/hYBMvbLW5j1OTaP9HESvSAiWCx7eEnrAFz8TzCmBboa1UyhaA +1jeas22Ua6MFxsYUKXlxtR1bVo1SEBnHFMpo+ERqwIBRoZHdF0mKf0U4YzGRBGpo +ZcaUbAVhnToRvyj8r5129Tm01ALFRQqlXp6N1Eh1r/1APOjHkPi4m+iaQoH8yhp5 +DHD+hhJXkG/+kBiWT38G8AOXU9V1Ckak4iFeqInxrCULQqkjbW9squXdnFTA/ycK +md/kR6bTqdehSbBc0O45KFzBxijRNmhlO2NaoQPzXEhpM0z+/Gop9RDQfdNits76 +GNMk1GnI+k5Ra3gxvIAbmPZEal4kdWicFDU9+Jgpc0EgbqeWHbbwjKMuVloTQ0BK +4oByn5iy+wChEJhclEk0dSVRnMQKuyHJAIn4EsL5q/lhr85Kgd9dmcgGLoNHvSBW +spPQXwE87/Uji+60bYlxHV0RLpkZAx8aSoku3xutRv6NPfTSS37AvhxpTlp2SjmZ +KDlCcCZ2PSX8u10I/Xor+287AgMBAAGjfDB6MA4GA1UdDwEB/wQEAwIF4DAMBgNV +HRMBAf8EAjAAMB0GA1UdDgQWBBQ4DxuHc8kkS8lQcVPj/o/HkmcaBTAfBgNVHSME +GDAWgBSwB8V3vlY1dTSfKgSjFTSrWH69SDAaBgNVHREEEzARgg8qLndpbGRjYXJk +LnRlc3QwDQYJKoZIhvcNAQELBQADggIBAIGZ5BFULiJDd/GTZ0LtLCvyip2zk4pm +wXMKcXcNm73SZTJg1pYKTf1jePwpPKHXyZXNRlWgcy9SAhpvjTMKnJgg8GQAsc4i +5MfPft8ec+r2IguZHmp43qtZDdo/vZxSEaS/ANWlr/KlsCV83dIeBRrKOqx/gkpD +b4vo92QgZJz+A1v3RwsV4FJnvs0nKIBZuDtY9Tkhg3PbZKDiHqK8VgFXvXas6+82 +LDRbAePPi4sd/0uTAkwkuFe01YUqz/fTUWXW/9w8D8ncqkGFX1gx7j8IayrEVlTq +itBBSivzeymZxWSB6k+CyTT4YTIS0zyFC+IYFC6b+1O/io6iJEo4EELsUIho0BEo +QZaMQuYFEeGVastirGrJWdxP0QxhPe8A+Bjf5pgIsbDhn2VjvHGls/mrm847vH/Y +dtT6amek4rEjXfymHTTyPFDnLz+LWYwLOe2cS+cTco61GtlCMLiXS+l8AvbU3rsl +0Sqea+Zla+tHdzEmMHkeU9ghjtuHfOzSlB4FMRMHMDqaoQ8yOkDJ4ESXkbBBNhEx +PkbZrTAB+kh1LTsrsiDIX2r1IOgRUz151NCMKnh7XojgGrewt7Dj61337bD4itqf +8E4Cp6D3dd0zCMYudm1yCpaQH8q9bHNBtc62H6Z8UsUJp0QARuZRQZAfj5qq7d0P +4PEK1Z+dLLpB +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem new file mode 100644 index 000000000..3c9f22c5b --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF1TCCA72gAwIBAgIQDMbvu6na8/uUMbRL+PYXajANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIE9u +ZSBTaWduaW5nIENlcnQwHhcNMjYwNjE1MTczNDM4WhcNMjcwNjE1MTczNTM4WjBh +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMRcwFQYDVQQDEw5jdHJsMS13aWxkY2Fy +ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALdz2IJ9kZV42nBIIyKN +xTNnVYSNj3Y6NgP4dKiw9MvpeomAJ46Z5SBMcjP65huKxUS7+i6BGROdzaFcVzt7 +ZHVhLCs5K7/XCDPA18klbciIU8zfZKbS2FD5J5gA6xZWey8apButysnFpXlqquhX +dZPCNRex9Dllnc/hYBMvbLW5j1OTaP9HESvSAiWCx7eEnrAFz8TzCmBboa1UyhaA +1jeas22Ua6MFxsYUKXlxtR1bVo1SEBnHFMpo+ERqwIBRoZHdF0mKf0U4YzGRBGpo +ZcaUbAVhnToRvyj8r5129Tm01ALFRQqlXp6N1Eh1r/1APOjHkPi4m+iaQoH8yhp5 +DHD+hhJXkG/+kBiWT38G8AOXU9V1Ckak4iFeqInxrCULQqkjbW9squXdnFTA/ycK +md/kR6bTqdehSbBc0O45KFzBxijRNmhlO2NaoQPzXEhpM0z+/Gop9RDQfdNits76 +GNMk1GnI+k5Ra3gxvIAbmPZEal4kdWicFDU9+Jgpc0EgbqeWHbbwjKMuVloTQ0BK +4oByn5iy+wChEJhclEk0dSVRnMQKuyHJAIn4EsL5q/lhr85Kgd9dmcgGLoNHvSBW +spPQXwE87/Uji+60bYlxHV0RLpkZAx8aSoku3xutRv6NPfTSS37AvhxpTlp2SjmZ +KDlCcCZ2PSX8u10I/Xor+287AgMBAAGjfDB6MA4GA1UdDwEB/wQEAwIF4DAMBgNV +HRMBAf8EAjAAMB0GA1UdDgQWBBQ4DxuHc8kkS8lQcVPj/o/HkmcaBTAfBgNVHSME +GDAWgBSwB8V3vlY1dTSfKgSjFTSrWH69SDAaBgNVHREEEzARgg8qLndpbGRjYXJk +LnRlc3QwDQYJKoZIhvcNAQELBQADggIBAIGZ5BFULiJDd/GTZ0LtLCvyip2zk4pm +wXMKcXcNm73SZTJg1pYKTf1jePwpPKHXyZXNRlWgcy9SAhpvjTMKnJgg8GQAsc4i +5MfPft8ec+r2IguZHmp43qtZDdo/vZxSEaS/ANWlr/KlsCV83dIeBRrKOqx/gkpD +b4vo92QgZJz+A1v3RwsV4FJnvs0nKIBZuDtY9Tkhg3PbZKDiHqK8VgFXvXas6+82 +LDRbAePPi4sd/0uTAkwkuFe01YUqz/fTUWXW/9w8D8ncqkGFX1gx7j8IayrEVlTq +itBBSivzeymZxWSB6k+CyTT4YTIS0zyFC+IYFC6b+1O/io6iJEo4EELsUIho0BEo +QZaMQuYFEeGVastirGrJWdxP0QxhPe8A+Bjf5pgIsbDhn2VjvHGls/mrm847vH/Y +dtT6amek4rEjXfymHTTyPFDnLz+LWYwLOe2cS+cTco61GtlCMLiXS+l8AvbU3rsl +0Sqea+Zla+tHdzEmMHkeU9ghjtuHfOzSlB4FMRMHMDqaoQ8yOkDJ4ESXkbBBNhEx +PkbZrTAB+kh1LTsrsiDIX2r1IOgRUz151NCMKnh7XojgGrewt7Dj61337bD4itqf +8E4Cp6D3dd0zCMYudm1yCpaQH8q9bHNBtc62H6Z8UsUJp0QARuZRQZAfj5qq7d0P +4PEK1Z+dLLpB +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1.cert b/tests/testdata/pki/ctrl1/certs/ctrl1.cert new file mode 100644 index 000000000..123517c0c --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem b/tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem new file mode 100644 index 000000000..84cbb1219 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/server.cert b/tests/testdata/pki/ctrl1/certs/server.cert new file mode 100644 index 000000000..329042233 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/server.chain.pem b/tests/testdata/pki/ctrl1/certs/server.chain.pem new file mode 100644 index 000000000..e7dcffdb4 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/crlnumber b/tests/testdata/pki/ctrl1/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl1/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl1/index.txt b/tests/testdata/pki/ctrl1/index.txt new file mode 100644 index 000000000..96a030828 --- /dev/null +++ b/tests/testdata/pki/ctrl1/index.txt @@ -0,0 +1,7 @@ +V 270608194659Z 42BAB1AD44A3E68A09FAFA96D5053C55 server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl1 +V 270608194700Z 10F986EB4390F63470E06103115D027E client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl1 +V 270608194703Z A0489D9392A153CF99FDFE979C440300 001-server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=001 +V 270608194703Z F4C38A7DC40F7CA063E8898C02D1EFBD 001-client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=001 +V 270608194704Z C4B86735AA971B44B232966933AC0082 002-server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=002 +V 270608194704Z 232D098BD3001A3D417BEB7E3C7B85EA 002-client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=002 +V 270615173538Z 0CC6EFBBA9DAF3FB9431B44BF8F6176A ctrl1-wildcard.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl1-wildcard diff --git a/tests/testdata/pki/ctrl1/index.txt.attr b/tests/testdata/pki/ctrl1/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/ctrl1/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/ctrl1/keys/001-client.key b/tests/testdata/pki/ctrl1/keys/001-client.key new file mode 100644 index 000000000..edd50e251 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/001-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xNxIR9fQijm7VhVumF+XPU5LW3GLYg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/001-server.key b/tests/testdata/pki/ctrl1/keys/001-server.key new file mode 100644 index 000000000..edd50e251 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/001-server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDGAnn10g6qJbjd +v7WTMLe9PBUopw0O0IKd/s8KuB7lJXinYS4/OKWa5vuMYHCvdUTs43ITMYT1SAAA +3NHhxbG7sohtTBdilKB6KTiBJyal1WT8r0SkVtQDupL8f9qneeGBFWSxhOZ8Gh7V +iu4P6ZvMUazUriG0oIt/wdNvrR8vwjqYuHSYtSH+dJl+G6AWq2oDlLXujp9rYmBe +vstyEm51Xaqbs9I3g38XWtmycb1ppWpvTT/Q0YZrADm1j8KPdZj177nbieVyGixW +bD1mpFNRdobSq8CF5zL42EAj0HuF5UyI362kJavp1Q3cNCcZxK2WuvpM+sqLDTRj +wqb0wvlC0PXXJatHi7DTXycREIKfW3JZrS9e0xeliZxZ3N9eN6K1SRKNdFs5uIPT +lB2aYUUGper+14x6Zs3sP28iCf/soV+1ciuc5c2eKFvJtoOs13H0C5QEHF3eBQFG +xhfvyYJ9gWKHQd8ZqzcMZKGlx8AJ5f22BJTDUYx9bsdC7okEHGvxiBZZI00S/DhJ +0uZ/sq2MgLs+Nep9u95XLu2IENzJGHf3TPC1YLeZ6Rm6Dz7C9ODMk0i2DubXlxTT +nnxqizqPSYx6wsY5mhBkyvbKwJeeECFycXHg5udSDRMPAjD3N38HLMQC5HpPd+Py +EqLwDZHnvA7TfEJNnS0r6Nr+9oA8KwIDAQABAoICABcTU847gB0BD2W8QTdZjpgd +h+O4cBOhZg9ukgdQRL10m/Z5o0+X1OjT8U2/MXKpKnKm4PTuqO2CyglPsp/qj5dt +q2t9Wh5RbtA9vImEnALodv/hlxDF06hpllaZucwZrpeB/D2Z3NhqnNxK+ApfsZni +jM8uSdxId2lYl8DkfoVsz7JbkK75MPB7+yVQp9vsdwraykb2VQrJKKzx9DijgD7q +PwctMhrpFqcMneEnUykcL2awLHcz68PNf2wyyDw6uqwyl0jH7bbZ6P/bVl5/WPxd +4UFs5Z9VPcH1RahG1BZbDgvHcW93Om0iOJ7IbyQYIRi3SpMtCOyiic/0b8uHa0/T +FLkclI4zQdb7sSMs+URHbF1tI1rsJLjQweELGcVgTxRUZ6oeX3gKSZbsXpT9WYWL +eTGjOVRCaBaJR7kdL6URHLp+YGqjHhyK0GCczJQFTWRw4vSUVmrfSw6x5ArM5fcn +QqYrVKDcKqBWMS99EDr97Vk4pLEXcZBkiSb7JCkvTaOxwAzMDfcjRUbNj21olT+q +hPlr40/AXff3tlBeZEF2y8VbNclXYhWNbCep6qf25fwO+5nQwmbi7JBkNUEnGSPh +TTtEpLYTZM+YlomtGkzm+M7p85o5YWQyTGpx47oi5UYuSyM9I1HQgA71JgHKUXkP +aekzF9LW8FGniKKKeBytAoIBAQDaMr7xH3UmjvR4wRtoux8w7qZsrzQLRZQ+UO/a +AgBHGIzKM37H28D5ywQ5OZEIfgHVDTLXdMTqIfdPnnS2wS3smISgA/2pVQSWrjdL +yQ7J6ZEKlloQj5HLHCdH8noi+yGVV8hgN0D5aieSDER8Jklpx/gqVIU2w8yJpfqf +qUaJHz8A+yAtjSB9o/9nUy7qG+ANH98Zu5UzM/avLjjwI4ttFHvBoy3DghZ0FEj2 +JMSG1eiwtbMmghMGTAxtGd5CeMUCNo7bhmyAkF/twOYNIYbLLtUNQBOI/6NKDJE4 +IrhBN/Rd+DEiLXg2sapjZVayefix8ghoLqmf+ShUu7JxsYuNAoIBAQDoUFppWXeZ +TZI5gRBiB0/gNxTKp+KH4OrzBCjmzIZG3m/GHaLb+XMWp3lDaSCx51FW6rs+Y3H9 +K9U/yEi8/QGNA85ShKLjfK8On0MJ1iApAR0GI7WRF/LGLHOTktvNFU8+jiAAwErE +FdjyPcXGwhcGh9uVgbsCCrY4n7cCzFsQlQOp7JiizHXHnqD4pgDbhMb9UHV9D8PB ++bAiOGjuMJbpX7Ca5qzufqMQrPuUI27zYdKbcSW4xdmnnL2UpWOhfP5V2HVNGMtv +2S3tlHmAzGKLIQrOC+L0GVaVjaXZzcE+3ZqbZrEnFtxBSNKlicGAH3rEOgjex/DJ +5bX8yrYu8ZyXAoIBAQDHsmHmtG+3lmZQjgvT0k49rm1KUx9Coa+HSPP7hpFSgDl6 +ZZ7Gs2zuMMvww6rlJs3ue2HopsoBrox1pgQTrsGlJAFdoclqTJEihte82IpcaROB +qRFMztkUFvQseNMnaMfXsruEs40Xt/v3Qpr7NN9DnOyXcTtqX9C2ud2ien4/yQUU +lMFibyMpWgrKOetZ/6ES4fSBzJhEgG5o7djWjvFwJ6sMEZg0sr5yBo6wF/51mRiZ +gfIwcCORQ9CrmV09YOKNn/knGOP81iW1pzHlGqYPFxGOSvKWE8ESaGYQBlaz+c4A +osH1EcBP7lKPHZQ3TEI8OdNP5kUAUDyFMDOksmWRAoIBAC95pFBdMTzKBnTr9D4k +QmWOvBgCISAljb4l/nnUGCjJckwpaEvN+YAKr5RgGodgv1Wk2KkSR4w2dfj35C2D +AtsfiP2CD4uvwlKZp2iOMrpOePD1QTiqjTmEggYJgBO0JCKjhtTAd0cFM/WlPBK3 +PV9fSjeOcHolWEHQGeVff8iuuzXxnOvAHt2xhDHsMsappsTDuY7aNuYgMad8oazz +jxOYM44kT/jZNlfFEhCfASCJwDF1+QB+plFDjdafyfGJiqxedPJRe9map3Ei44W3 +vvexw/SBp1q9Bt2/OcJ98tz3Co5xeQiE+nP4/ttHgJy/UiZhuchVUdbgs8U6Y+rK +A3cCggEAbWj5NNDv8JuXOpQVtvyAS5v6UJYgZpX7+Ts11hXANK5NwsxVoKM7PlQ3 +2PKEcT5eWpiygj76e2/jxcSlW7smlxh05Q8Pi/zHaYOVtBRygfA7znZHIlWv+OFh +UJUJrXhe/Tg/MoaQy/tagcwVY9Nt7lh4XPhH6ENkO9OLy8W9XcPhMytpOWE6cWnr +bghpz6yxp6/ZbG9tPW5oFmKkDQJp4oF79fl5uGn4UqKZvh9jBdrCKgFHORrVam+f +iJtK+D/PgeTs5XGUOC2otWc5rZMPCDiT4wrgNkxnxlkIqQ3OlszKRjs0/gfvZL6H +xNxIR9fQijm7VhVumF+XPU5LW3GLYg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/001.key b/tests/testdata/pki/ctrl1/keys/001.key new file mode 100644 index 000000000..edd50e251 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/001.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xNxIR9fQijm7VhVumF+XPU5LW3GLYg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/002-client.key b/tests/testdata/pki/ctrl1/keys/002-client.key new file mode 100644 index 000000000..f747b52df --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/002-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +JEwDoz4jrGsQIvYnrDygS1DUgU8BXw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/002-server.key b/tests/testdata/pki/ctrl1/keys/002-server.key new file mode 100644 index 000000000..f747b52df --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/002-server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +JEwDoz4jrGsQIvYnrDygS1DUgU8BXw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/002.key b/tests/testdata/pki/ctrl1/keys/002.key new file mode 100644 index 000000000..f747b52df --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/002.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDI492ZM6/QBOJ+ +sWIrCl7ws9UvsK2cN59HVUWfXqp8/mr7y8fW0CqOdYcJHuKhr5W0ZVquZV8HkOLq +L7rBjlW//3pwqOmjgCX0H0DLWyuBr/Y6IIP0D8qa4Hj5RtPvWa9G8U5+d2z19qZN +gRvZL79fHiKFlMW0ParI480mCeR8u+QNx+vNwLkWPjtN2TucaNdArlrpSLlpmW+E +6SvBaql1W9g7F+g3mHC1TUsPmyrGkE0TTWNyslsNb2VwJTOOkE65YW5ltTZygcaj +h1j8ZfFqn/hMy+B90WNeFAN3xzLSxQRiOnD4MptcbQKNTvWgxBS5OagaKIWWi7FW +sGsmMSztTIQ4U8K6oSAnmhNgEcDcvSPvj4Zq8o/Wx2q2qqTo8W83FEazoEuohg5f +ubQZrhDPkrOr3fEmm54JGjE670UdVMUSY7bnWtYGf6Mn8B5zdDDSWWvH/PmnT/KP +KHdo24obRujZfb0kYEzcD7GBhJPeut3NEZpIHGrdXuueFtiYOYb77V/ODC+jB7gF +ObEj6YyQDiBsihqeMmOCvg0IGeS3T7E5O+cqvnPei9rfGsHEw64XOyLvhjgE3AbE +vZkAAX0sqnmtcNXo81yDAidnhHE3QGiUdgTi4+W8+Ns3iWXzjulllc/RFefL39OU +R5oeuhxjp0fP0QPhILJXiMgvaopFIwIDAQABAoICABZHTvgCh2jmYcfzHBPx3n2L +NAVJ7rb4ZC2hA0udUAL0pCCwhMUJ6O5LkmIsjq2nr06GPvxAOb25D7ExAeEdS90z +E/0Sfnana44bOTBUOAr13LStjnSum6V5Z3EdrbtJkuqnMDFORUMxy1elDdWUOgDu +cp2l1hcbD6mfucySJEjA/ZWZqkjzKpOQ6zrC8J1z8ws1Ste8PPO9FGUFBtk4Xvqo +6N4E1Lf1q+ovXDeq2Z+TuTh+yJybswVWaUV6mrEgx9o/N+MHqbYhNkpEZFX5aECO +5RZ/NbI+WmrAhXHvIW/GcaoDGSwtUJV7cWECdLMTi8jO4BmmjMoZS911Syy9H2Iv +RNIaTQ5/I6H+cJ5BXl8j4QgSafBWieKI/nbbBk336RBG4HT0BbK16qqCvjOBscbd +QswFpNUViJwrN4J3UVQ5ov9LGzFPugYbrMsIBc5WovaW20EwehPCAQo5TJHsw41Z +80X/7tK5txh9hBK/qvCncJebksK74U7BEJXOvJWR0z9j+DhIkXBGFyPdGfvvmqDU +L71v8pFu9+5Lw+4O1y5ZGlECpWh/tiCMseYU8TokxjlhVJWvBYqFDO3IiM+0kBZJ +t1cnDfumMQZuELyWT4JrpIf94xmRTX5Q6+TGLs8CMYAxW//SQrdWar29ypRTlBob +77A54QUhorRpNPeu0sxhAoIBAQDUJNVR5Rf8ZtX+MdpWEZfyggtwj0nTAhsvmWdj +eL7d0FFjptpUHufctMg9XYyQBbSF6OX73fLmW8R2AxOq0KU/lGbW7o20LoWF+13I +lQ+FXDh8u9tNclTCvL/RPyP9/Vp16czVKeUBm57Dli6U/NI9WSYmoarp1ZpQHrOq +ExPgLxRaEHDVE8bH29NM4K9AddXWYaOmr+h6WErMYv3z0sskDK8r2VCF+YCU7bgg +kuDoHlMwqaAAS/xhLzXU1/3Ll3SwbcyZpY2PJ5WtSEU/4PhKgn+WUD8yxzy/xXnu +gLpl9hr4Et7mPBaCg7+Kwt7ULLFI5teTtkS/rKKnxKjYbsFnAoIBAQDya3SCP2LQ +CyMKq61lA6UVBGBEFwPNDcPx8/lsL6bUHNkDBSfl715VMG+0Y0sK+lHkBBIXBSha +dwrhwuAokM3sE8EsU0EshlRkxZA4IeSNJZIMhoA1ZsSrwWzqfnXZRWM2Imo9C8HR +CDuVxe8xm9PosVkjztQxdFdnTbr342AOFBfJtThGH6KAIJl4y6D8Q/A4khCc+In9 +CFstdvkA1LvVrOsaZRgsaOMmCe/t8y8qBnYkwFrO7l+9yPJTvXpXt/go5+ua2Oiz +FRBN/036T2j54XHrAko2J2P1qTHEeR4Sz/wJ76AbkyMUQbx8TRnsHtYDTKzyukXr +PiHKkD65MBzlAoIBAQChlcSd8j+I0tNgIJzLPe9cmc0Y2StD+7C1WsUzMP9AeLHl +k2ts83Vr2I9EnoK4GIBeFv1GENI4v+Euej16uB2GBgUm5OEuQtkVKldOtqrxy0KD +T5tErDb/dUEtokhJ57YFZiXMn3J8/Qm6tCOa+88vRz4V4sIKBdbZ++ihPJLBCVsZ +Fri6s6uPA1M4lVMnaBmOhyRdjFMpDSM79pK0KvTr6nVqksYQpfBYf5DlzrpcUuzO +fgUO9NGxPIJmMnZvolcRIzDaPw1J4r7RE+EbPMIiDrAbz9ezV6pf74xmK8rOatwz +5Du/ZjlHBA7bJH8RYVFP0/ST4BsRW+8wlkx9Pi81AoIBAEjTTyTxtdWaAuTpviVi +eu1day4JW/L1969wA7YkepGuOONXYS0KQ+SDsIsYaSrbGE7Pij+wJPdIW9SGnqCR +H8uUA6w1NpcRo7jCQRkb1XUxnDZ3EXz5aovOh9HKO80Rs0RIAnJkXc1WE9BirgDR +N0/oN0cykzxoHvnBuDFP9UNFdf/6mPU3+7FSGcjYI3ylTD0yZBtD7vA82mdGHnfJ +cMQ4NkivphtxRwveYF2zeK+/US6pOp5/rfNtsp7FhUNk2QAw6+6QyxdBdKx5zgss +X/TxbS+UeIbcPoNGWNUVLZupL6YsfoTKJvem5+DPqMHuYYyMNEiW/Po2qCAaRT2M +3qECggEAVN4DPXy6Vu+n7La4DMlEZ10TLXtHUKZvZu/9QQ9zZw8YlCTHRqSseTcO +Nc1tZDBPv2bfV8KL0UxuOU1xwOz3FCadhd2XYzlwv9tfz7IEyAVOm5t56MgTlP9Y +NtiYL8L5QrWgn3xK9ZK/Ko7gqk5CABeE4VOxbtqq8/nK74kZLWdS2id6QNU1FiNe +B1CIXPk0rUUKtRRQ92JFasxO2jTQ02sDnL+dlG4JpbIIuCCZ4t38UFLO8q9zK4nB +6ndTN+oy0IB7qI8FOu/skAQCCDZRncrxFbo9L+8t9Uh7Jxk3H3NdtJRVGkJFCN0b +JEwDoz4jrGsQIvYnrDygS1DUgU8BXw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/client.key b/tests/testdata/pki/ctrl1/keys/client.key new file mode 100644 index 000000000..b09506b65 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +GS310cbh3YMphOQfnvPdUmpYosUE+mA= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key b/tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key new file mode 100644 index 000000000..0e1becded --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +nA2XaI04MOuQS6f+KbxCKjPm4wAqTg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/ctrl1.key b/tests/testdata/pki/ctrl1/keys/ctrl1.key new file mode 100644 index 000000000..00fde317e --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/ctrl1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +0m5V3q28vSoG+oMh8tb52ou7ehAx +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/server.key b/tests/testdata/pki/ctrl1/keys/server.key new file mode 100644 index 000000000..a6a2c2928 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +iZ8vcOG7Ef5P8ozXCTY2gC0LgAwtGE0= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/serial b/tests/testdata/pki/ctrl1/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl1/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl2/certs/client.cert b/tests/testdata/pki/ctrl2/certs/client.cert new file mode 100644 index 000000000..79dbe6884 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +Ux+nTcEJiFavZWYRwExpVLHPoNt5h6Q= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/client.chain.pem b/tests/testdata/pki/ctrl2/certs/client.chain.pem new file mode 100644 index 000000000..5f619e09d --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +Ux+nTcEJiFavZWYRwExpVLHPoNt5h6Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/ctrl2.cert b/tests/testdata/pki/ctrl2/certs/ctrl2.cert new file mode 100644 index 000000000..d16b1d146 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/ctrl2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF4zCCA8ugAwIBAgIQIn0Td/JpTeNMlziQeIGDBTANBgkqhkiG9w0BAQsFADBk +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMRowGAYDVQQDExFaaXRpIFRlc3QgUm9v +dCBDQTAeFw0yNjA2MDgwNzQ2NThaFw0zNjA2MDUxOTQ3MDBaMG4xCzAJBgNVBAYT +AlVTMRIwEAYDVQQHEwlDaGFybG90dGUxEzARBgNVBAoTCk5ldEZvdW5kcnkxEDAO +BgNVBAsTB0FEVi1ERVYxJDAiBgNVBAMTG0NvbnRyb2xsZXIgVHdvIFNpZ25pbmcg +Q2VydDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALZGIhSruH9XdJJj +7lkN56mGGjENnK6H7WaBy2Whx0vmWdITbGn82EciIoMdRgDmOzttIpc/qkEmsM8/ +80XCtVrKqeLOtaAMwDh8OQkg7E8O9bynSHpo9ibjsrOKkJJBKFTCW/zEmV/nnWx6 +PlyUlsLcs7glzm3Ho/B076yetj+h7e45OyseTT2U0/b24hYYWzcqUGzVN/mUGWJ0 +ZikL3YIc4n8DsqErHEbySIfjGFeQTL+oPf+eHAbECg4DxhAEYbpP3iMYYzowuNSS +kmbzAy4zift4sxQwqSqIXQpNYwk854rQDQIScHtkvuMdE50QW9OE6uivJxQ+fsjU +eR+neNwwpe/TOUAz/+6OhByt8O1ZaIIzROwLpBGGBknskwu5gKGzODKYQhxZoGDf +d2gkGfFAkBSUYoeSUfU6ng11kqLeR80L6AFz7uIBJHurZpoKx2VhHyht0ksBFabc +vxXtHoamUsDTB+NxPZPo9MW0tr2i9CKsiqAFMdZCg2IABrzMCL0czalqW+BUoEEd +M7Q8Ktn2qL0H0kC9YlVE0pNe3/+Gl/muMrAovOaSczn1vu2BSZJ7osoAgpoFhgHf +PIF5rk6rEPqXi0EpuVoefzB1ysSFfuAJhCd3DXTVTrfx/VL2jXLdB3iL9j1WL72L +jqoCS2P990/wXelAJUGxKOhzyNFXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGG +MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFH7Pj+tHTk24/Hyq1ptc2HJ6 ++r4kMB8GA1UdIwQYMBaAFLrUicEbQHtcxurj45pW8Rgv8nt6MB0GA1UdEQQWMBSG +EnNwaWZmZTovL3ppdGkudGVzdDANBgkqhkiG9w0BAQsFAAOCAgEAfZ8KEnrZE0Td +Y1gP5WMEwsgZ37+EPYy/CYEakK0vwQs72B24sHkNxa8Dw0cvuj/5cSnCTzXkKGWx +DxkLf2HdakeU8R7GZwx7jvdzaBJkkOLHKQFfz1IN9dsHDHo6duqUEwWqq+oFqwvS +zFMHOiqQWyqP0arfsiKit3o2r+JzTVHRPRziZ+m4m0mjyNsK6OKuY5M9lwIJiS+4 +mpeiFP8NVE5bQuIeXQsl/KTWBHuXzlm/baywf/yrm0mFaOeXAzy8QkhMbDWXzSQ5 +n7oJscpv9proVD1wcEVRKfQLzgcFZ/im76LXiAkRe1iVBPCN0YeP7zjlaePn1uyF +D2Ihnze1nigodsAvDubxJe5DGcAUyACIrZ9U0OUn0VZgeyE5AQL8au9XbXpxxS6h +tUS6r0I91y2W5QVZXq918QJlPHtHfCL+QlZVwC8wQr2rqGc4NnR2qhMoyDu4bqAh +uk2qnDZAxU6mx+5pIHtLQgaNS0cbEjnBL69z7kNM/+IpAh4/oqWxGFTTtNS5DVsJ +l8lI/gT3fwO/SCy7FOkyZ/FPXv4NMRPDIY0VrjlrRjqmJFtf/UtgPVSRGu2quyXs +CadTmjrGXf1jEy03wsDnBjC4rZ7UVZGLtiColFH7Hx33Blp24XPzkhGdMS0qvnJd +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem b/tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem new file mode 100644 index 000000000..bc0b38f90 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/server.cert b/tests/testdata/pki/ctrl2/certs/server.cert new file mode 100644 index 000000000..0be56eafe --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIQSVxp5FK1FgizYm7h4uH0/zANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIFR3 +byBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAxWhcNMjcwNjA4MTk0NzAxWjBY +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALeAzoNPjeodnKM6757o1p2HeUKZ9Sjk +3jwf0YKJZ88ZyotseFXRheCWVcIaprvnEiSU2xxU9DcQNKCL1sdGoyHvA17VVxOz +uRAmVOvcleL4P+3zsfwnv2SG+9tsRSDBNRqjiBnb312cWpLPZFvhA2neYX7rKbGf +eMWswPuo9txNyKW6w/0gwqdg4VYJlNMau0iyR4R0tqMj2ujcyHOualkQ9UHof9vA +9SO/3XbbgVKerS1NRRifkAlkBZmWRN33FRckJhs/TBmgeylKBKph74LIda5qvOCV +D+aEj8rZ088yAJvebpwf84d1J+6oJLM2Jg7pZrRF17qjcEX8Ws2UWnZ1h9tfsLpc +o35f3AzxCAy0qPNIj3AK2ea4d+xOceq8z4K9C6pYsmlDykyXYb6ajpRXNtW7PUl+ +CaSSf9PcYpz6uos0pqqe1dm1CtBaMY7EAYPJKdjDCoKOmdnGpmBeAeN2+XiBiuJn +sm+LfR5JI3NTne76/oTcFAsSL7MvmuuSfZFGi0ITEZJSnW40Si6yNuaFqIL1w7jo +24Cao1qPUb4Oq7b2d6BE6kS0CQrsXqo4SHmZlCqPWszMF4I7XlcPFYrNAwLFvHh8 +ELhXFi5edhG1b9Ev4S8wn45xr8kg8SoX0tkMG09eGwItKv2d8YYmvNCZVjsXPUY8 +9QlM1GReJMadAgMBAAGjgaIwgZ8wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB/wQC +MAAwHQYDVR0OBBYEFJONrbM0VVrFZGIz4dJublQXLBDdMB8GA1UdIwQYMBaAFH7P +j+tHTk24/Hyq1ptc2HJ6+r4kMD8GA1UdEQQ4MDaCCWxvY2FsaG9zdIcEfwAAAYYj +c3BpZmZlOi8veml0aS50ZXN0L2NvbnRyb2xsZXIvY3RybDIwDQYJKoZIhvcNAQEL +BQADggIBADVe4P4lrgXQLf7lYPPtH23EujMw77eL7vKgGhnrUNqwB3N1Bl2rNaEd +wTCQVTR5QMNAbHCP/FMrlDot76wKpIg6b7bkfKmIV+89YtTIZ4Q7PORtj9Dch7wF +FO+a01YELZM6rNbdACvnBDOm44ofN5rbGZ+EkJKtZjjqcXo0VEV25qiX9wrzEkar +Dsm8cru/7oJ5nyiYl0XCVoOWg7s1ENKWAjUWAo5vDUCaLpILac0SH+9EW94jZSVq +b93/FyMLQ4TMHc8OTH2x6QdcICqV1RswMD9X7ZAqVl+uGnwO9bZSO3364Vh0Rtx4 +eU9aBZqWrkmWCy4ufZ+7OgBfm3CCIyQjD8Ro/dZyrqSYORG8TCmQAXtCkorjar0H +w10Z+K9YVcIrVMo48n3TafI8WOxTVe+IStEw9yPOpVEasLKpwZEhz5DQgky9KvOR +n3+Xz3aiBz0V4XtkAycDjX0cXPNH92McsAcU4xa1RJZZo42aB+jjJKrtja3zB9fb +dtpHcBoOWzlfPAP3ln89HYH1OlzMmics30sMVwOt0JnARGd00wziPVBeIkRTygcD +X11QRhnpbgj6pnogvy22aUkvv+Evc6sZ1MJrwtaq/NWgdotrLPfM6nHat49Mv8x4 +GrBCYbe/Emui8/Mom/nCw1MuBgRUXgJxJvH9HmvU4e8pDNGNxwZX +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/server.chain.pem b/tests/testdata/pki/ctrl2/certs/server.chain.pem new file mode 100644 index 000000000..4f2979d32 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIQSVxp5FK1FgizYm7h4uH0/zANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIFR3 +byBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAxWhcNMjcwNjA4MTk0NzAxWjBY +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALeAzoNPjeodnKM6757o1p2HeUKZ9Sjk +3jwf0YKJZ88ZyotseFXRheCWVcIaprvnEiSU2xxU9DcQNKCL1sdGoyHvA17VVxOz +uRAmVOvcleL4P+3zsfwnv2SG+9tsRSDBNRqjiBnb312cWpLPZFvhA2neYX7rKbGf +eMWswPuo9txNyKW6w/0gwqdg4VYJlNMau0iyR4R0tqMj2ujcyHOualkQ9UHof9vA +9SO/3XbbgVKerS1NRRifkAlkBZmWRN33FRckJhs/TBmgeylKBKph74LIda5qvOCV +D+aEj8rZ088yAJvebpwf84d1J+6oJLM2Jg7pZrRF17qjcEX8Ws2UWnZ1h9tfsLpc +o35f3AzxCAy0qPNIj3AK2ea4d+xOceq8z4K9C6pYsmlDykyXYb6ajpRXNtW7PUl+ +CaSSf9PcYpz6uos0pqqe1dm1CtBaMY7EAYPJKdjDCoKOmdnGpmBeAeN2+XiBiuJn +sm+LfR5JI3NTne76/oTcFAsSL7MvmuuSfZFGi0ITEZJSnW40Si6yNuaFqIL1w7jo +24Cao1qPUb4Oq7b2d6BE6kS0CQrsXqo4SHmZlCqPWszMF4I7XlcPFYrNAwLFvHh8 +ELhXFi5edhG1b9Ev4S8wn45xr8kg8SoX0tkMG09eGwItKv2d8YYmvNCZVjsXPUY8 +9QlM1GReJMadAgMBAAGjgaIwgZ8wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB/wQC +MAAwHQYDVR0OBBYEFJONrbM0VVrFZGIz4dJublQXLBDdMB8GA1UdIwQYMBaAFH7P +j+tHTk24/Hyq1ptc2HJ6+r4kMD8GA1UdEQQ4MDaCCWxvY2FsaG9zdIcEfwAAAYYj +c3BpZmZlOi8veml0aS50ZXN0L2NvbnRyb2xsZXIvY3RybDIwDQYJKoZIhvcNAQEL +BQADggIBADVe4P4lrgXQLf7lYPPtH23EujMw77eL7vKgGhnrUNqwB3N1Bl2rNaEd +wTCQVTR5QMNAbHCP/FMrlDot76wKpIg6b7bkfKmIV+89YtTIZ4Q7PORtj9Dch7wF +FO+a01YELZM6rNbdACvnBDOm44ofN5rbGZ+EkJKtZjjqcXo0VEV25qiX9wrzEkar +Dsm8cru/7oJ5nyiYl0XCVoOWg7s1ENKWAjUWAo5vDUCaLpILac0SH+9EW94jZSVq +b93/FyMLQ4TMHc8OTH2x6QdcICqV1RswMD9X7ZAqVl+uGnwO9bZSO3364Vh0Rtx4 +eU9aBZqWrkmWCy4ufZ+7OgBfm3CCIyQjD8Ro/dZyrqSYORG8TCmQAXtCkorjar0H +w10Z+K9YVcIrVMo48n3TafI8WOxTVe+IStEw9yPOpVEasLKpwZEhz5DQgky9KvOR +n3+Xz3aiBz0V4XtkAycDjX0cXPNH92McsAcU4xa1RJZZo42aB+jjJKrtja3zB9fb +dtpHcBoOWzlfPAP3ln89HYH1OlzMmics30sMVwOt0JnARGd00wziPVBeIkRTygcD +X11QRhnpbgj6pnogvy22aUkvv+Evc6sZ1MJrwtaq/NWgdotrLPfM6nHat49Mv8x4 +GrBCYbe/Emui8/Mom/nCw1MuBgRUXgJxJvH9HmvU4e8pDNGNxwZX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/crlnumber b/tests/testdata/pki/ctrl2/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl2/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl2/index.txt b/tests/testdata/pki/ctrl2/index.txt new file mode 100644 index 000000000..602efa132 --- /dev/null +++ b/tests/testdata/pki/ctrl2/index.txt @@ -0,0 +1,2 @@ +V 270608194701Z 495C69E452B51608B3626EE1E2E1F4FF server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl2 +V 270608194701Z 99E7ACDD30109FCF741C1B50D5734475 client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl2 diff --git a/tests/testdata/pki/ctrl2/index.txt.attr b/tests/testdata/pki/ctrl2/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/ctrl2/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/ctrl2/keys/client.key b/tests/testdata/pki/ctrl2/keys/client.key new file mode 100644 index 000000000..d26af536a --- /dev/null +++ b/tests/testdata/pki/ctrl2/keys/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +GMCP9cRBczHq3SwCtS05fdJZS1YP +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl2/keys/ctrl2.key b/tests/testdata/pki/ctrl2/keys/ctrl2.key new file mode 100644 index 000000000..d24cbbcc3 --- /dev/null +++ b/tests/testdata/pki/ctrl2/keys/ctrl2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +eyhEiBE1bGvtRz2pOW2AwJ/4o2oX3w== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl2/keys/server.key b/tests/testdata/pki/ctrl2/keys/server.key new file mode 100644 index 000000000..e0d66d328 --- /dev/null +++ b/tests/testdata/pki/ctrl2/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +tmOMH+mLTElAKmn0PX+kD0XMbYcB390= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl2/serial b/tests/testdata/pki/ctrl2/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl2/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl3/certs/client.cert b/tests/testdata/pki/ctrl3/certs/client.cert new file mode 100644 index 000000000..614a63132 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF5TCCA82gAwIBAgIRAJR+Z3r5ca2rcP81499c/i4wDQYJKoZIhvcNAQELBQAw +cDELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEmMCQGA1UEAxMdQ29udHJvbGxlciBU +aHJlZSBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAyWhcNMjcwNjA4MTk0NzAy +WjBYMQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJk6/PetUwUgp9/NGNa9XHCsZrVV +zKdvdY72nHUn25xrssWYUmhn1cbC4Zk1LkIOC2ZCjN4E2ghELtMrQC+ehTq5bw46 +XpVR2Oe/5vwp7bKEggI87Zm71axN+qQNImpcivzYzbN2XXaS9aglDxADay5QmkJK +fJN5fTdWgSO9DPrysbiiZB/Rq+Hy7ZIb7WqSJNcB+Ikk8zhIENyJl6IfT0mBSmKx +B1imeqJFAQI1E7nDC571yg0CiRal2/aBLSXSL2xhp9imtsWz4AUj2SM/PwSF+YeO +MPucBQqEio8LiJv5yRf4y9++bekhWdEPgzl8c7Ok7ipVuEu2jGL3DdYmIT40c25L +jS9CgH8Nn0A5KOeS0LMQ7zak7LRepJUkMCP9nMzITVZos5xrmhEfwEGgA4mdsTwy +1+yC+dOyzhx3QHD0wcOlJbbHgZ+dxFA6LbQ2SYj6ACT/SDYCG6BeBLTFHmPgDXRN +hOJVHOYqTuOjDFGCkTwpWe6ZA5DZzuhA0fSingM2GAFv4s1liIsm5FW3sFZm/8uf +peukW+qUZm9s7ItxKxFyO5D4GNyvhDsoDVYhOnnjviHvBlH4BH6njMIkdKIZPcrs ++St/n3L0IwHAH6Z+z+Ss3/jy6cYg5OBrEQBi+icsTaQfv/xb1yg6Vazs+iOST/HY +bN8NL1AcGtdpgFJFAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB +/wQCMAAwHQYDVR0OBBYEFIwk5BkFpTrvwTixqKqXyauIZ8bEMB8GA1UdIwQYMBaA +FEXOuRtwkd7kHv3/yzcUKCUgZiEwMC4GA1UdEQQnMCWGI3NwaWZmZTovL3ppdGku +dGVzdC9jb250cm9sbGVyL2N0cmwzMA0GCSqGSIb3DQEBCwUAA4ICAQCNwLVbetaH +P84InOu1AcwVMUdkN9OD8Tl/16eQ3V8p1+3awRPqKS5nQDUciw0YTU85QeQobdY2 +mgPlmcBlD7l/EcVXQRTCnC0MLFbEkhqjd0UxCcAt8TT8OWCGr88eFECrXBP7YUIh +oIprnqlrJyCFcS4rwgSDP3PqB0JULxjfTsJUWAKcsx9g48m32yXYo5o2h8x0WnwJ +X1/HKyqDytshATF/zEaA8dx+CvyvGrK957jv8Q3T/Ggz7+D8Cj6o9cGd3B8XcQOM +lfzOQBvJoUQClLC2kit4TQPvpLedH/ggfFQI7Lu+WBONlodco6M+5jmUg1bFFZnf +JVQ9BqYJ80WvXLgti5lwD2ETV+U6CBv869mFO/LPyUsmSXn6u7qtzugx/AqvASjp +cG7JE+Nchse5souPSRUerkUI2yJTEwUz1XPKtpy0bfqYY9M+82hU3MizdVp8Th7N +S2LzARGfwrIEf7prjTQGV7mfrMB6FP1hoGxVBLJTn0D6uWUkbhzq+f+2MIXkLel4 +sIWHzgcfbh8myHBHHh/K3vk+XNigNqXGft4tD0INVOJd6nfIMFXVtSiPFAKhH1Rt +nOFj9oYlfmDLgEDcJwE+eHYi3ciYtdC1QuCsehz4ZJ/z72w/cU+5Lusu91zlUYnW +xPulh0qLGQYUplsS+oZLne3DESbfB7tvTQ== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/client.chain.pem b/tests/testdata/pki/ctrl3/certs/client.chain.pem new file mode 100644 index 000000000..8bd3e413a --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF5TCCA82gAwIBAgIRAJR+Z3r5ca2rcP81499c/i4wDQYJKoZIhvcNAQELBQAw +cDELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEmMCQGA1UEAxMdQ29udHJvbGxlciBU +aHJlZSBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAyWhcNMjcwNjA4MTk0NzAy +WjBYMQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJk6/PetUwUgp9/NGNa9XHCsZrVV +zKdvdY72nHUn25xrssWYUmhn1cbC4Zk1LkIOC2ZCjN4E2ghELtMrQC+ehTq5bw46 +XpVR2Oe/5vwp7bKEggI87Zm71axN+qQNImpcivzYzbN2XXaS9aglDxADay5QmkJK +fJN5fTdWgSO9DPrysbiiZB/Rq+Hy7ZIb7WqSJNcB+Ikk8zhIENyJl6IfT0mBSmKx +B1imeqJFAQI1E7nDC571yg0CiRal2/aBLSXSL2xhp9imtsWz4AUj2SM/PwSF+YeO +MPucBQqEio8LiJv5yRf4y9++bekhWdEPgzl8c7Ok7ipVuEu2jGL3DdYmIT40c25L +jS9CgH8Nn0A5KOeS0LMQ7zak7LRepJUkMCP9nMzITVZos5xrmhEfwEGgA4mdsTwy +1+yC+dOyzhx3QHD0wcOlJbbHgZ+dxFA6LbQ2SYj6ACT/SDYCG6BeBLTFHmPgDXRN +hOJVHOYqTuOjDFGCkTwpWe6ZA5DZzuhA0fSingM2GAFv4s1liIsm5FW3sFZm/8uf +peukW+qUZm9s7ItxKxFyO5D4GNyvhDsoDVYhOnnjviHvBlH4BH6njMIkdKIZPcrs ++St/n3L0IwHAH6Z+z+Ss3/jy6cYg5OBrEQBi+icsTaQfv/xb1yg6Vazs+iOST/HY +bN8NL1AcGtdpgFJFAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB +/wQCMAAwHQYDVR0OBBYEFIwk5BkFpTrvwTixqKqXyauIZ8bEMB8GA1UdIwQYMBaA +FEXOuRtwkd7kHv3/yzcUKCUgZiEwMC4GA1UdEQQnMCWGI3NwaWZmZTovL3ppdGku +dGVzdC9jb250cm9sbGVyL2N0cmwzMA0GCSqGSIb3DQEBCwUAA4ICAQCNwLVbetaH +P84InOu1AcwVMUdkN9OD8Tl/16eQ3V8p1+3awRPqKS5nQDUciw0YTU85QeQobdY2 +mgPlmcBlD7l/EcVXQRTCnC0MLFbEkhqjd0UxCcAt8TT8OWCGr88eFECrXBP7YUIh +oIprnqlrJyCFcS4rwgSDP3PqB0JULxjfTsJUWAKcsx9g48m32yXYo5o2h8x0WnwJ +X1/HKyqDytshATF/zEaA8dx+CvyvGrK957jv8Q3T/Ggz7+D8Cj6o9cGd3B8XcQOM +lfzOQBvJoUQClLC2kit4TQPvpLedH/ggfFQI7Lu+WBONlodco6M+5jmUg1bFFZnf +JVQ9BqYJ80WvXLgti5lwD2ETV+U6CBv869mFO/LPyUsmSXn6u7qtzugx/AqvASjp +cG7JE+Nchse5souPSRUerkUI2yJTEwUz1XPKtpy0bfqYY9M+82hU3MizdVp8Th7N +S2LzARGfwrIEf7prjTQGV7mfrMB6FP1hoGxVBLJTn0D6uWUkbhzq+f+2MIXkLel4 +sIWHzgcfbh8myHBHHh/K3vk+XNigNqXGft4tD0INVOJd6nfIMFXVtSiPFAKhH1Rt +nOFj9oYlfmDLgEDcJwE+eHYi3ciYtdC1QuCsehz4ZJ/z72w/cU+5Lusu91zlUYnW +xPulh0qLGQYUplsS+oZLne3DESbfB7tvTQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/ctrl3.cert b/tests/testdata/pki/ctrl3/certs/ctrl3.cert new file mode 100644 index 000000000..d0c74a596 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/ctrl3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem b/tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem new file mode 100644 index 000000000..5e275e3a9 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/server.cert b/tests/testdata/pki/ctrl3/certs/server.cert new file mode 100644 index 000000000..baf93bec7 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/server.chain.pem b/tests/testdata/pki/ctrl3/certs/server.chain.pem new file mode 100644 index 000000000..b511da8ed --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/crlnumber b/tests/testdata/pki/ctrl3/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl3/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl3/index.txt b/tests/testdata/pki/ctrl3/index.txt new file mode 100644 index 000000000..1608c060d --- /dev/null +++ b/tests/testdata/pki/ctrl3/index.txt @@ -0,0 +1,2 @@ +V 270608194702Z 50E4CACA9B71F50108FC0D4656297B72 server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl3 +V 270608194702Z 947E677AF971ADAB70FF35E3DF5CFE2E client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl3 diff --git a/tests/testdata/pki/ctrl3/index.txt.attr b/tests/testdata/pki/ctrl3/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/ctrl3/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/ctrl3/keys/client.key b/tests/testdata/pki/ctrl3/keys/client.key new file mode 100644 index 000000000..c40543d3c --- /dev/null +++ b/tests/testdata/pki/ctrl3/keys/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +UmJ3wX2J8nbWkv0jDPNpZS1bAdvsSg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl3/keys/ctrl3.key b/tests/testdata/pki/ctrl3/keys/ctrl3.key new file mode 100644 index 000000000..d627f9bbb --- /dev/null +++ b/tests/testdata/pki/ctrl3/keys/ctrl3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +OuO7d1g9Ek/alqalOhx/fpRu9ZO4Sw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl3/keys/server.key b/tests/testdata/pki/ctrl3/keys/server.key new file mode 100644 index 000000000..d33be67cf --- /dev/null +++ b/tests/testdata/pki/ctrl3/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +fxDKHr5e6RElhWIDnvjxEn2vGdhwfQ== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl3/serial b/tests/testdata/pki/ctrl3/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl3/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/root/certs/ctrl1.cert b/tests/testdata/pki/root/certs/ctrl1.cert new file mode 100644 index 000000000..123517c0c --- /dev/null +++ b/tests/testdata/pki/root/certs/ctrl1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/certs/ctrl2.cert b/tests/testdata/pki/root/certs/ctrl2.cert new file mode 100644 index 000000000..d16b1d146 --- /dev/null +++ b/tests/testdata/pki/root/certs/ctrl2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/certs/ctrl3.cert b/tests/testdata/pki/root/certs/ctrl3.cert new file mode 100644 index 000000000..d0c74a596 --- /dev/null +++ b/tests/testdata/pki/root/certs/ctrl3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/certs/root.cert b/tests/testdata/pki/root/certs/root.cert new file mode 100644 index 000000000..93c5eade7 --- /dev/null +++ b/tests/testdata/pki/root/certs/root.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/crlnumber b/tests/testdata/pki/root/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/root/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/root/index.txt b/tests/testdata/pki/root/index.txt new file mode 100644 index 000000000..ff5f48994 --- /dev/null +++ b/tests/testdata/pki/root/index.txt @@ -0,0 +1,4 @@ +V 360605194658Z 6F39C81E1BBE69E5A88FC2E6FC870B3D root.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Ziti Test Root CA +V 360605194658Z 1BD94A4C2C917927AC9231CAE00C6AC4 ctrl1.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller One Signing Cert +V 360605194700Z 227D1377F2694DE34C97389078818305 ctrl2.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Two Signing Cert +V 360605194702Z E694727C36315D5E094BC539D0FE4339 ctrl3.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Three Signing Cert diff --git a/tests/testdata/pki/root/index.txt.attr b/tests/testdata/pki/root/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/root/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/root/keys/ctrl1.key b/tests/testdata/pki/root/keys/ctrl1.key new file mode 100644 index 000000000..00fde317e --- /dev/null +++ b/tests/testdata/pki/root/keys/ctrl1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCj9dl0nTsVXPHa +jDTvFw+TimPLC/543QX/lmnMLU3YWDqMpSj0g2PYATWjAMG6nH1dg7ZmDNXZnIud +zjB//ul13Oqqn7bCTYBMzWdinMTIfrUhBgrfLVJolwmiw3utnuU+hBnepPZhiy0t +UKx8VHLK3qE9g/BnkMgBFmwFf6DHPg0x+PSCBGCvQsl4TC3CKnt+IF1f7Estn2sp +A5SHZH1LO0XM8kt6Phuxz05IgK0YSEjvH4kFuSE2lbGPQglnqJZj14AVCxF6MRUj +XDJaSEsYiZ4uPlexwD+h1e0K3xLAqDvyxF7C/C2BXZTrCGBpb3TVGOsNu6zf1Cqo +Sgpf/JZPNlLzeS+7ek/G/Cwz7Ap5xsK+DOZf6SSwAH3H2uwRG5YKsdFELGkxH3Ew +QcG3b6TJVlo294sdNYt1UB7uvlxYw3kfWXYcxoaqJSCLr0gn7mFmIavhFnAlqHHh +uNZ+qva3un2cB52tN4AQj5b8xvlA2bgzii1rl0eI88eMwgYuhpcAuhlyzqOQphRb +u2WBOLtRyB5I/ttG1J4pJUglQUoXQE/nsl4VksiW1zJ8XQ+Hb0RwhnkJAJsiNNFK +qcKmCkjMglc9RnTfzOP9mO4dj7DwPrMKwNnE5qgwtROIn6vdkHAyI+NVTdw8YDDN +V0NZWePVyrWzHNrqBy6oIZcVN90dZwIDAQABAoICAC7dbGEhOgyiqvyxe8XlXQ3q +jhixHnUUlAzYxPfX8TrICUA/SyQM1EKfIeIsKrO43DqZFc84lv2i+eNK1uEXD0sh +sK/BhB8owOXzBjyRG8xFL2e3ju74yOfdWCM+ZgEb/GGwp6ZUl5oNCoY723mUN9WV +6henuVUY9Joe+xRdRSr+KQ5iHx10u+AMooKwn5myw+aqwJXU+C4btakc/Vzv08Jn +uE1a6kkQLKFX5IPjx9Y7fyFeba+FmaE9C2or0X1gGlCCfflF1yKKmgSn6zqUFGb4 +mw6TwkQr8+RBvgYP+g+4Zp4/E+j+5NDn21OM6uXoNkhc3X7o6IJ35hOBSlLiY2He +rU8nezykfpCg+qjeve6FKzMTmDJRMbZIYAB+T4JqjkvGwbPpihdGRnJlBM6+DsTC +QBNnZw4wEKymp4pT7iGffzzLUlJCOZIAHFcOsuG+qNh3bpS0UIvBx6aaM9v+DcHa +8cf/wFLNFdhDQEv0DYpK63R/QxExJ9UwP4Ugf+KTrbNZS/j/sEBpxXdJB91MNWi5 +rX85pZ0x0PzdKiXTHjWE2mI88ZIb/ZdL17Vm8exfyTvAWJdFnVioZkzeqhvCI1JH +JMXjzuRsAXKw6nbA+pJppRcrHs2pdY+qx2JDVsZY/5FU3GVTiCcDup6Z4Jhxy6HW +ez/13LVzTuLHkcEc4d9dAoIBAQDFdB9KUPlN/PrhzYHeL/q7uDv49nfNmyfvQ/vT +ATKZl10fxqGZH6j/CGXdsgd+HFXPfqpJOSSJvyBvuKeVgCqSx6ngey3S/vEQG5Fn +vmeDXRl3rqlB9iCguH4o83qWmjjYxaFsp71AcF3q/jneOZ9NCasp7r2TsEeEV/nC +9lfha5LC3c7p0HTD3xjiFQdJ15pMwilcUlJ1QPlk0PTcgIxIODr/6YEUmdlmVMg+ +L+kp+F7f6w3YPKVcerW5uESa85GQgOppHXIvOQ3NP8E+UfMlz6yDxNCKxt/oxJBV +aFWJiuXS0ZYrQQUx7LljUa0jhEXdmQlpIEfOS/AggHQYBsgTAoIBAQDUk2TzaRW+ +tHQWAaiqty+0CiuBitHf1GbIR3VCurvug6v9x0eJDuEiusOpKT1Cb6SbuJ3thsMh +7gI4NVcGvkV+97ZWfgR2CWy76XHs7skp7IoMXSN0jQq3t6JqFj9X/cp3UcjhfKnt +ePoDRkpiFwAekhn/syuMNBw6b5bukRWeHvj4bKLMYSHh+jWNRHIWe/N2DeuzqSkv +ofjqIGHXP0R6hCCfPlTwG3CHb312AsiXegjJfL8c6JDsjQmXhM1vQ3VqXxRsU3D+ +KK659f01oZsBgyQSpichtRcmXgeC1YRMmAoJgPVsuYF8IGspwGbtQ/fzcCuagYaR +ku2Yr5PoSqfdAoIBAC8sG+GcUMMyAhn6B+G2IrfAPwuujlafj73YxwvVCGqrP8M8 +qBS1/KDZN8TsKGAXkuSchUAzF6iU8cHfIqJT2VfxvYL0yrDS2XKYs3dOhNpcXp46 +KxOoIoljKjjMWmgqdhRLutIDjPIdJkLi855Es+squSqub7od7igPAIt0YPBoy8ok +Ra+UbqDw5rf0gCZDDQjzhgAZZru+hxZv2V/okhsa2/WRqpXqX4bUEHbS6WhufvQN +6uPTMUpTwqCZBkLil88nDVmJgGMJxWNYrOkfmPBamgNs/Ml607l/ZGATKgRPG7Lv +AWpaAUy5Gl1BARUwH6TeT+I+pQkDGV4aciHfVOMCggEALfmK1dIeb9ZbXP8S2Ykw ++gFRE31QktY/PIWn6Ly2NImpwwM8h3n+WyKFeqp+o0W+FifBkEObJFVziXCP19eC +9Eji2KX8lQLIz4NXrmSegUC1QqNKLcTrUnyW1dbl8EPlbBT2Gz55CfEmMVscb0aG +MhZrJRA9FN+YU1MbE5GxWTddpWzpcMZ5K4SP3HO3MQGx0BCGr56gV7ryOMC0KHd4 +ef7lh0tV13A30DLesY08kPZFvD4Mn1X1MhP2xRxlyfCPDmht5FfPkkh+MZ3wG49O +FO+l95qT0Ah4b0Xa3gMLz/z5/sAzVEZyqMPiKW+BU0Nl9vKFm67zybw7QtCGbrDm +yQKCAQA9lChrSe7ti7WYvF4VxtwCt88dAXqxcx1m4zV2g8917FjgXDatIEvrGL4q +JAlYp9fH6a4+4oHMwnlgPYFIjSp2tuPV4lI+t/Od9x98OVhd+7AJwKhQIhA0wEh5 ++t2lhsurIzNQCitG8BtwVS+3I8G76/Ao++ZQ/wF6980TwTjLDPe3ca/ENQ4NTDa6 +jTyjPQso1pbL2CaAejx8Rx3SiF9y0YzBR0LAA1xPh5T0mbnigwqcAPKq6Exu4xqb +WWPhgW2inU6HOq7kjX9BBngxLLUnp/O0ff1x5hcbIF1uPyYBROFEiDAidG0zXcAA +0m5V3q28vSoG+oMh8tb52ou7ehAx +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/keys/ctrl2.key b/tests/testdata/pki/root/keys/ctrl2.key new file mode 100644 index 000000000..d24cbbcc3 --- /dev/null +++ b/tests/testdata/pki/root/keys/ctrl2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +eyhEiBE1bGvtRz2pOW2AwJ/4o2oX3w== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/keys/ctrl3.key b/tests/testdata/pki/root/keys/ctrl3.key new file mode 100644 index 000000000..d627f9bbb --- /dev/null +++ b/tests/testdata/pki/root/keys/ctrl3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +OuO7d1g9Ek/alqalOhx/fpRu9ZO4Sw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/keys/root.key b/tests/testdata/pki/root/keys/root.key new file mode 100644 index 000000000..dbfd9dfe4 --- /dev/null +++ b/tests/testdata/pki/root/keys/root.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +X3LLrLWLVE+ST/a3Y4U7VReDAhLejQ== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/serial b/tests/testdata/pki/root/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/root/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing-root/certs/signing-bundle.pem b/tests/testdata/pki/signing-root/certs/signing-bundle.pem new file mode 100644 index 000000000..9cdfc4815 --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing-bundle.pem @@ -0,0 +1,136 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing-root.cert b/tests/testdata/pki/signing-root/certs/signing-root.cert new file mode 100644 index 000000000..2ab71cddb --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing-root.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing1.cert b/tests/testdata/pki/signing-root/certs/signing1.cert new file mode 100644 index 000000000..64eb58ddd --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing2.cert b/tests/testdata/pki/signing-root/certs/signing2.cert new file mode 100644 index 000000000..61e7d692a --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing3.cert b/tests/testdata/pki/signing-root/certs/signing3.cert new file mode 100644 index 000000000..b718a78dd --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/crlnumber b/tests/testdata/pki/signing-root/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing-root/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing-root/index.txt b/tests/testdata/pki/signing-root/index.txt new file mode 100644 index 000000000..b44468338 --- /dev/null +++ b/tests/testdata/pki/signing-root/index.txt @@ -0,0 +1,4 @@ +V 360717203902Z 3B87C2F4998E4F147C9B17A09A3928A0 signing-root.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Ziti Test Edge Signing Root CA +V 360717203912Z B7F4FFFF0F1124C93D28FB1FAB1FB1F3 signing1.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller One Edge Signing Cert +V 360717203925Z 697438320D95080B2FD7D6F235000003 signing2.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Two Edge Signing Cert +V 360717203941Z 08252B39FA6BFF5F4A5C9713247987A0 signing3.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Three Edge Signing Cert diff --git a/tests/testdata/pki/signing-root/index.txt.attr b/tests/testdata/pki/signing-root/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing-root/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing-root/keys/signing-root.key b/tests/testdata/pki/signing-root/keys/signing-root.key new file mode 100644 index 000000000..67a75560e --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing-root.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +VJEDpvAeB2DHyNUEeZtmnx7cHnGc5rA= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/keys/signing1.key b/tests/testdata/pki/signing-root/keys/signing1.key new file mode 100644 index 000000000..3f76626cf --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 ++zChBm7yqi6IpiHHBzba61+8R4k3OVk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/keys/signing2.key b/tests/testdata/pki/signing-root/keys/signing2.key new file mode 100644 index 000000000..7b5235ee1 --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xaHRILYXaGEXR2iffVG2IXE3jH8skw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/keys/signing3.key b/tests/testdata/pki/signing-root/keys/signing3.key new file mode 100644 index 000000000..11c20f3e8 --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +CYWpy35FrNj2ymT1QJR+XU2Gn4GzJgg= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/serial b/tests/testdata/pki/signing-root/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing-root/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing1/certs/signing1.cert b/tests/testdata/pki/signing1/certs/signing1.cert new file mode 100644 index 000000000..64eb58ddd --- /dev/null +++ b/tests/testdata/pki/signing1/certs/signing1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing1/certs/signing1.chain.pem b/tests/testdata/pki/signing1/certs/signing1.chain.pem new file mode 100644 index 000000000..295fb57f5 --- /dev/null +++ b/tests/testdata/pki/signing1/certs/signing1.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing1/crlnumber b/tests/testdata/pki/signing1/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing1/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing1/index.txt b/tests/testdata/pki/signing1/index.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/pki/signing1/index.txt.attr b/tests/testdata/pki/signing1/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing1/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing1/keys/signing1.key b/tests/testdata/pki/signing1/keys/signing1.key new file mode 100644 index 000000000..3f76626cf --- /dev/null +++ b/tests/testdata/pki/signing1/keys/signing1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 ++zChBm7yqi6IpiHHBzba61+8R4k3OVk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing1/serial b/tests/testdata/pki/signing1/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing1/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing2/certs/signing2.cert b/tests/testdata/pki/signing2/certs/signing2.cert new file mode 100644 index 000000000..61e7d692a --- /dev/null +++ b/tests/testdata/pki/signing2/certs/signing2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing2/certs/signing2.chain.pem b/tests/testdata/pki/signing2/certs/signing2.chain.pem new file mode 100644 index 000000000..e4e5aa192 --- /dev/null +++ b/tests/testdata/pki/signing2/certs/signing2.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing2/crlnumber b/tests/testdata/pki/signing2/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing2/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing2/index.txt b/tests/testdata/pki/signing2/index.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/pki/signing2/index.txt.attr b/tests/testdata/pki/signing2/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing2/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing2/keys/signing2.key b/tests/testdata/pki/signing2/keys/signing2.key new file mode 100644 index 000000000..7b5235ee1 --- /dev/null +++ b/tests/testdata/pki/signing2/keys/signing2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xaHRILYXaGEXR2iffVG2IXE3jH8skw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing2/serial b/tests/testdata/pki/signing2/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing2/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing3/certs/signing3.cert b/tests/testdata/pki/signing3/certs/signing3.cert new file mode 100644 index 000000000..b718a78dd --- /dev/null +++ b/tests/testdata/pki/signing3/certs/signing3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing3/certs/signing3.chain.pem b/tests/testdata/pki/signing3/certs/signing3.chain.pem new file mode 100644 index 000000000..de3cdee71 --- /dev/null +++ b/tests/testdata/pki/signing3/certs/signing3.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing3/crlnumber b/tests/testdata/pki/signing3/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing3/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing3/index.txt b/tests/testdata/pki/signing3/index.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/pki/signing3/index.txt.attr b/tests/testdata/pki/signing3/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing3/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing3/keys/signing3.key b/tests/testdata/pki/signing3/keys/signing3.key new file mode 100644 index 000000000..11c20f3e8 --- /dev/null +++ b/tests/testdata/pki/signing3/keys/signing3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +CYWpy35FrNj2ymT1QJR+XU2Gn4GzJgg= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing3/serial b/tests/testdata/pki/signing3/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing3/serial @@ -0,0 +1 @@ +01 diff --git a/ziti/cmd/cmd.go b/ziti/cmd/cmd.go index f352e69dc..e656e5718 100644 --- a/ziti/cmd/cmd.go +++ b/ziti/cmd/cmd.go @@ -72,6 +72,7 @@ var rootCommand = RootCmd{ cobraCommand: &cobra.Command{ Use: "ziti", Short: "ziti is a CLI for working with Ziti", + SilenceErrors: true, // errors are printed by exitWithError, this prevents cobra from also printing them PersistentPreRun: func(cmd *cobra.Command, args []string) { cmd.SilenceUsage = true }, diff --git a/ziti/cmd/common/version.go b/ziti/cmd/common/version.go index ae1b53655..8066de7af 100644 --- a/ziti/cmd/common/version.go +++ b/ziti/cmd/common/version.go @@ -18,7 +18,9 @@ package common import ( "fmt" + "strings" + "github.com/openziti/ziti/v2/common/build" "github.com/openziti/ziti/v2/common/version" "github.com/spf13/cobra" ) @@ -36,6 +38,10 @@ func NewVersionCmd() *cobra.Command { fmt.Printf("Build Date: %s\n", version.GetBuildDate()) fmt.Printf("Go Version: %s\n", version.GetGoVersion()) fmt.Printf("OS/Arch: %s/%s\n", version.GetOS(), version.GetArchitecture()) + + if buildFlags := build.GetBuildFlags(); len(buildFlags) > 0 { + fmt.Printf("Build Flags: %s\n", strings.Join(buildFlags, ", ")) + } } else { fmt.Println(version.GetVersion()) } diff --git a/ziti/cmd/edge/login.go b/ziti/cmd/edge/login.go index 172ea59b5..9ec0c1ffd 100644 --- a/ziti/cmd/edge/login.go +++ b/ziti/cmd/edge/login.go @@ -757,7 +757,24 @@ func (o *LoginOptions) Login() (edge_apis.ApiSession, error) { return edge_apis.NewApiSessionLegacy(o.Token), nil } } else if o.ApiSession != nil { - return o.ApiSession, nil + if o.ApiSession.GetType() != edge_apis.ApiSessionTypeOidc || !util.OidcAccessTokenExpired(o.ApiSession) { + return o.ApiSession, nil + } + // The cached OIDC access token is expired. It can only be refreshed if the cached refresh + // token is itself still valid, otherwise the user must authenticate from scratch. + if !util.OidcRefreshTokenValid(o.ApiSession) { + return nil, fmt.Errorf("the cached access token has expired and the refresh token can no longer be used to re-authenticate, please login again") + } + if o.mgmtClient == nil { + return nil, fmt.Errorf("cannot refresh the access token, no management client is available") + } + o.Println("Access token has expired, refreshing it using the cached refresh token...") + refreshed, refreshErr := o.mgmtClient.AuthenticateWithPreviousSession(&edge_apis.EmptyCredentials{}, o.ApiSession) + if refreshErr != nil || refreshed == nil { + return nil, fmt.Errorf("failed to refresh the access token using the cached refresh token, please login again: %w", refreshErr) + } + o.Println("Successfully refreshed the access token") + return refreshed, nil } else if o.Username != "" && o.Password != "" { authCreds = edge_apis.NewUpdbCredentials(o.Username, o.Password) } else if o.ClientCert != "" || o.ClientKey != "" { diff --git a/ziti/cmd/pki/pki.go b/ziti/cmd/pki/pki.go index 281953fbd..8a19d7f9e 100644 --- a/ziti/cmd/pki/pki.go +++ b/ziti/cmd/pki/pki.go @@ -64,6 +64,7 @@ type PKIFlags struct { SpiffeID string AllowOverwrite bool EcCurve string + NotBefore string } var ( diff --git a/ziti/cmd/pki/pki_create.go b/ziti/cmd/pki/pki_create.go index 2853fa14d..87b43eae8 100644 --- a/ziti/cmd/pki/pki_create.go +++ b/ziti/cmd/pki/pki_create.go @@ -88,6 +88,8 @@ func (options *PKICreateOptions) addPKICreateFlags(cmd *cobra.Command) { err = options.viper.BindPFlag("pki-province", cmd.PersistentFlags().Lookup("pki-province")) options.panicOnErr(err) + cmd.PersistentFlags().StringVarP(&options.Flags.NotBefore, "not-before", "", "", "Certificate notBefore time as RFC3339 (2006-01-02T15:04:05Z) or date (2006-01-02); defaults to now") + // cmd.PersistentFlags().StringVarP(&options.Flags.PKIProvince, "pki-state", "", "NC", "State/Province") // cmd.MarkFlagRequired("pki-state") // options.viper.BindPFlag("pki-state", cmd.PersistentFlags().Lookup("pki-state")) @@ -281,7 +283,7 @@ func (o *PKICreateOptions) ObtainFileName(caFile string, commonName string) stri } // ObtainPKIRequestTemplate returns the 'template' used in the PKI request -func (o *PKICreateOptions) ObtainPKIRequestTemplate(commonName string) *x509.Certificate { +func (o *PKICreateOptions) ObtainPKIRequestTemplate(commonName string) (*x509.Certificate, error) { subject := pkix.Name{CommonName: commonName} if str := o.viper.GetString("pki-organization"); str != "" { @@ -306,7 +308,27 @@ func (o *PKICreateOptions) ObtainPKIRequestTemplate(commonName string) *x509.Cer MaxPathLen: o.Flags.CAMaxPath, } - return template + if o.Flags.NotBefore != "" { + notBefore, err := parseNotBefore(o.Flags.NotBefore) + if err != nil { + return nil, err + } + template.NotBefore = notBefore + } + + return template, nil +} + +// parseNotBefore parses a certificate notBefore value supplied as either an RFC3339 timestamp or a +// date-only string (interpreted as midnight UTC). +func parseNotBefore(value string) (time.Time, error) { + if t, err := time.Parse(time.RFC3339, value); err == nil { + return t, nil + } + if t, err := time.Parse("2006-01-02", value); err == nil { + return t, nil + } + return time.Time{}, fmt.Errorf("invalid not-before [%s], expected RFC3339 (2006-01-02T15:04:05Z) or date (2006-01-02)", value) } // ObtainKeyName returns the private key from the key-file diff --git a/ziti/cmd/pki/pki_create_ca.go b/ziti/cmd/pki/pki_create_ca.go index a1a44cc32..f668875e5 100644 --- a/ziti/cmd/pki/pki_create_ca.go +++ b/ziti/cmd/pki/pki_create_ca.go @@ -104,7 +104,10 @@ func (o *PKICreateCAOptions) Run() error { commonName := o.Flags.CAName filename := o.ObtainFileName(caFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = true diff --git a/ziti/cmd/pki/pki_create_client.go b/ziti/cmd/pki/pki_create_client.go index fd6437ff9..f66bb2b47 100644 --- a/ziti/cmd/pki/pki_create_client.go +++ b/ziti/cmd/pki/pki_create_client.go @@ -108,7 +108,10 @@ func (o *PKICreateClientOptions) Run() error { } filename := o.ObtainFileName(clientCertFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = false template.EmailAddresses = o.Flags.Email diff --git a/ziti/cmd/pki/pki_create_intermediate.go b/ziti/cmd/pki/pki_create_intermediate.go index e8985117f..c70822d7e 100644 --- a/ziti/cmd/pki/pki_create_intermediate.go +++ b/ziti/cmd/pki/pki_create_intermediate.go @@ -102,7 +102,10 @@ func (o *PKICreateIntermediateOptions) Run() error { commonName := o.Flags.IntermediateName filename := o.ObtainFileName(intermediateFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = true diff --git a/ziti/cmd/pki/pki_create_key.go b/ziti/cmd/pki/pki_create_key.go index 0d5f3b88f..594245e1f 100644 --- a/ziti/cmd/pki/pki_create_key.go +++ b/ziti/cmd/pki/pki_create_key.go @@ -96,7 +96,10 @@ func (options *PKICreateKeyOptions) Run() error { return fmt.Errorf("%s", err) } - template := options.ObtainPKIRequestTemplate("") + template, err := options.ObtainPKIRequestTemplate("") + if err != nil { + return err + } var signer *certificate.Bundle signer, err = options.Flags.PKI.GetCA(caName) diff --git a/ziti/cmd/pki/pki_create_server.go b/ziti/cmd/pki/pki_create_server.go index 5248af371..1672e2dd3 100644 --- a/ziti/cmd/pki/pki_create_server.go +++ b/ziti/cmd/pki/pki_create_server.go @@ -114,7 +114,10 @@ func (o *PKICreateServerOptions) Run() error { } filename := o.ObtainFileName(serverCertFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = false template.IPAddresses = IPs diff --git a/ziti/pki/pki/template.go b/ziti/pki/pki/template.go index 4dcc30846..786a2cbdf 100644 --- a/ziti/pki/pki/template.go +++ b/ziti/pki/pki/template.go @@ -43,7 +43,9 @@ func defaultTemplate(genReq *Request, publicKey crypto.PublicKey) error { } genReq.Template.SerialNumber = sn - genReq.Template.NotBefore = time.Now().Add(-time.Minute) + if genReq.Template.NotBefore.IsZero() { + genReq.Template.NotBefore = time.Now().Add(-time.Minute) + } return nil } diff --git a/ziti/util/identities.go b/ziti/util/identities.go index ff867181d..4120f568e 100644 --- a/ziti/util/identities.go +++ b/ziti/util/identities.go @@ -16,6 +16,7 @@ import ( "time" httptransport "github.com/go-openapi/runtime/client" + "github.com/golang-jwt/jwt/v5" "github.com/openziti/edge-api/rest_management_api_client" edge_apis "github.com/openziti/sdk-golang/edge-apis" "github.com/openziti/sdk-golang/ziti" @@ -189,6 +190,86 @@ func (self *RestClientEdgeIdentity) NewRequest(client *resty.Client) *resty.Requ return r } +// oidcTokenLeeway skews the OIDC token expiration slightly before its true expiry so it is +// used on the border, requests won't fail 401s. +const oidcTokenLeeway = 30 * time.Second + +// OidcAccessTokenExpired reports whether sess is an OIDC session whose access token is expired (or +// will expire within oidcTokenLeeway). Non-OIDC sessions always report false. An unreadable token is +// treated as expired so the caller refreshes rather than sending a token it cannot validate. +func OidcAccessTokenExpired(sess edge_apis.ApiSession) bool { + oidcSess, ok := sess.(*edge_apis.ApiSessionOidc) + if !ok { + return false + } + claims, err := oidcSess.GetAccessClaims() + if err != nil { + return true + } + exp, err := claims.GetExpirationTime() + if err != nil || exp == nil { + return true + } + return time.Now().Add(oidcTokenLeeway).After(exp.Time) +} + +// OidcRefreshTokenValid reports whether sess carries an OIDC refresh token that is present and not +// yet expired. A missing, unreadable, or expired refresh token returns false, indicating the user +// must authenticate again rather than refresh. +func OidcRefreshTokenValid(sess edge_apis.ApiSession) bool { + oidcSess, ok := sess.(*edge_apis.ApiSessionOidc) + if !ok || oidcSess.OidcTokens == nil || oidcSess.OidcTokens.RefreshToken == "" { + return false + } + claims := &jwt.RegisteredClaims{} + if _, _, err := jwt.NewParser().ParseUnverified(oidcSess.OidcTokens.RefreshToken, claims); err != nil { + return false + } + exp, err := claims.GetExpirationTime() + if err != nil || exp == nil { + return false + } + return time.Now().Add(oidcTokenLeeway).Before(exp.Time) +} + +// refreshOidcTokenIfExpired refreshes the cached OIDC access token when it has expired and the +// refresh token is still valid. It returns true when the session was refreshed so the caller can +// persist the updated config. Non-OIDC sessions and still-valid access tokens are no-ops. An expired +// refresh token, or a failed refresh, returns an error telling the user to log in again. +func (self *RestClientEdgeIdentity) refreshOidcTokenIfExpired() (bool, error) { + if self.ApiSession == nil || self.ApiSession.ApiSession == nil { + return false, nil + } + if !OidcAccessTokenExpired(self.ApiSession.ApiSession) { + return false, nil + } + if !OidcRefreshTokenValid(self.ApiSession.ApiSession) { + return false, errors.New("the cached access token has expired and the refresh token can no longer be used to re-authenticate, please login again") + } + + ctrlUrl, err := url.Parse(self.Url) + if err != nil { + return false, errors.Wrapf(err, "could not parse controller url %v while refreshing token", self.Url) + } + + tlsClientConfig, err := self.NewTlsClientConfig() + if err != nil { + return false, err + } + + _, _ = fmt.Fprintln(os.Stderr, "Access token has expired, refreshing it using the cached refresh token...") + + mgmtClient := edge_apis.NewManagementApiClient([]*url.URL{ctrlUrl}, tlsClientConfig.RootCAs, nil) + refreshed, refreshErr := mgmtClient.AuthenticateWithPreviousSession(&edge_apis.EmptyCredentials{}, self.ApiSession.ApiSession) + if refreshErr != nil || refreshed == nil { + return false, errors.Wrap(refreshErr, "failed to refresh the access token using the cached refresh token, please login again") + } + + self.ApiSession = &edge_apis.ApiSessionJsonWrapper{ApiSession: refreshed} + _, _ = fmt.Fprintln(os.Stderr, "Successfully refreshed the access token") + return true, nil +} + func (self *RestClientEdgeIdentity) GetBaseUrlForApi(api API) (string, error) { if api == EdgeAPI { return self.Url, nil @@ -367,6 +448,18 @@ func LoadSelectedIdentity() (RestClientIdentity, error) { return nil, errors.Errorf("no identity '%v' found in CLI config %v. You can select an existing identity using 'ziti edge use '", id, configFile) } } + + // refresh an expired OIDC access token before any request uses it, persisting the new tokens + if refreshed, err := clientIdentity.refreshOidcTokenIfExpired(); err != nil { + return nil, err + } else if refreshed { + // the refresh already succeeded server side, so use the new token even if saving fails, + // otherwise a write error would waste a single-use refresh token + if persistErr := PersistRestClientConfig(config); persistErr != nil { + _, _ = fmt.Fprintf(os.Stderr, "warning: could not save refreshed token to CLI config: %v\n", persistErr) + } + } + selectedIdentity = clientIdentity } return selectedIdentity, nil diff --git a/ziti/util/identities_token_test.go b/ziti/util/identities_token_test.go new file mode 100644 index 000000000..912afa5f6 --- /dev/null +++ b/ziti/util/identities_token_test.go @@ -0,0 +1,105 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/stretchr/testify/require" +) + +// makeJwtWithExp builds a signed JWT carrying only an exp claim. The token helpers parse it +// unverified, so the signing key is irrelevant. +func makeJwtWithExp(t *testing.T, exp time.Time) string { + tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(exp), + }) + s, err := tok.SignedString([]byte("test-signing-key")) + require.NoError(t, err) + return s +} + +func TestOidcAccessTokenExpired(t *testing.T) { + now := time.Now() + + t.Run("non-oidc session is never treated as expired", func(t *testing.T) { + legacy := edge_apis.NewApiSessionLegacy("zt-session-token") + require.False(t, OidcAccessTokenExpired(legacy)) + }) + + t.Run("token expiring well in the future is not expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(time.Hour)), "refresh") + require.False(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("already expired token is expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), "refresh") + require.True(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("token expiring inside the leeway window is treated as expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(5*time.Second)), "refresh") + require.True(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("token expiring just past the leeway window is not expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(oidcTokenLeeway+time.Minute)), "refresh") + require.False(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("unreadable token is treated as expired so the caller refreshes", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc("not-a-jwt", "refresh") + require.True(t, OidcAccessTokenExpired(sess)) + }) +} + +func TestOidcRefreshTokenValid(t *testing.T) { + now := time.Now() + + t.Run("non-oidc session has no refresh token", func(t *testing.T) { + legacy := edge_apis.NewApiSessionLegacy("zt-session-token") + require.False(t, OidcRefreshTokenValid(legacy)) + }) + + t.Run("missing refresh token is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(time.Hour)), "") + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("unreadable refresh token is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(time.Hour)), "not-a-jwt") + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("expired refresh token is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(-time.Minute))) + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("refresh token expiring inside the leeway window is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(5*time.Second))) + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("unexpired refresh token is valid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(time.Hour))) + require.True(t, OidcRefreshTokenValid(sess)) + }) +} diff --git a/ziti/util/rest.go b/ziti/util/rest.go index 9de2025fe..fd23fc60e 100644 --- a/ziti/util/rest.go +++ b/ziti/util/rest.go @@ -103,8 +103,7 @@ func ControllerDetailEntity(api API, entityType, entityId string, logJSON bool, } if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error listing %v in Ziti Edge Controller. Status code: %v, Server returned: %v", - queryUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("listing %v", queryUrl), resp) } if logJSON { @@ -165,8 +164,7 @@ func ControllerList(api API, path string, params url.Values, logJSON bool, out i } if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error listing %v in Ziti Edge Controller. Status code: %v, Server returned: %v", - queryUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("listing %v", queryUrl), resp) } if logJSON { @@ -251,6 +249,37 @@ func WrapIfApiError(err error) error { return err } +// controllerResponseError builds a concise error for a non-success controller response. Authentication +// failures get an actionable hint instead of the raw server body, and other failures surface the API +// error's code and message rather than dumping the full JSON envelope and metadata. +func controllerResponseError(action string, resp *resty.Response) error { + return controllerResponseErrorFromParts(action, resp.StatusCode(), resp.Status(), resp.Body()) +} + +// controllerResponseErrorFromParts is the testable core of controllerResponseError. +func controllerResponseErrorFromParts(action string, statusCode int, status string, body []byte) error { + if statusCode == http.StatusUnauthorized { + return fmt.Errorf("not authorized: your session is invalid or has expired, please run 'ziti edge login' again (%s)", status) + } + if statusCode == http.StatusForbidden { + return fmt.Errorf("not authorized: this identity does not have permission to perform this action (%s)", status) + } + if msg := parseApiErrorMessage(body); msg != "" { + return fmt.Errorf("error %s. Status code: %s, %s", action, status, msg) + } + return fmt.Errorf("error %s. Status code: %s", action, status) +} + +// parseApiErrorMessage extracts the "CODE - message" summary from a controller API error envelope, +// returning "" when the body is not a recognizable error envelope. +func parseApiErrorMessage(body []byte) string { + env := &rest_model.APIErrorEnvelope{} + if err := json.Unmarshal(body, env); err != nil || env.Error == nil { + return "" + } + return formatApiError(env.Error) +} + type ClientOpts interface { OutputRequestJson() bool OutputResponseJson() bool @@ -318,8 +347,7 @@ func ControllerCreate(api API, entityType string, body string, out io.Writer, lo } if resp.StatusCode() != http.StatusCreated { - return nil, fmt.Errorf("error creating %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("creating %v instance", entityType), resp) } if logResponseJson { @@ -373,8 +401,7 @@ func ControllerDelete(api API, entityType string, id string, body string, out io if resp.StatusCode() != http.StatusOK { statusCode := resp.StatusCode() - return &statusCode, fmt.Errorf("error deleting %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityPath, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return &statusCode, controllerResponseError(fmt.Sprintf("deleting %v instance", entityPath), resp) } if logResponseJson { @@ -416,8 +443,7 @@ func ControllerUpdate(api API, entityType string, body string, out io.Writer, me } if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusAccepted { - return nil, fmt.Errorf("error updating %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("updating %v instance", entityType), resp) } if logResponseJSON { @@ -466,8 +492,7 @@ func EdgeControllerVerify(entityType, id, body string, out io.Writer, logJSON bo } if resp.StatusCode() != http.StatusOK { - return fmt.Errorf("error verifying %v instance (%v) in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityType, id, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return controllerResponseError(fmt.Sprintf("verifying %v instance (%v)", entityType, id), resp) } if logJSON { @@ -500,8 +525,7 @@ func EdgeControllerRequest(entityType string, out io.Writer, logJSON bool, timeo } if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error performing request [%s] %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - request.Method, entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("performing request [%s] %v instance", request.Method, entityType), resp) } if logJSON { diff --git a/ziti/util/rest_error_test.go b/ziti/util/rest_error_test.go new file mode 100644 index 000000000..46ced0f63 --- /dev/null +++ b/ziti/util/rest_error_test.go @@ -0,0 +1,66 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestControllerResponseErrorFromParts(t *testing.T) { + const unauthorizedBody = `{ + "error": {"code": "UNAUTHORIZED", "message": "The request could not be completed. The session is not authorized or the credentials are invalid", "requestId": "aszEJJLVr"}, + "meta": {"apiEnrollmentVersion": "0.0.1", "apiVersion": "0.0.1"} + }` + + t.Run("401 returns a clean login hint and no server body", func(t *testing.T) { + err := controllerResponseErrorFromParts("listing identities", http.StatusUnauthorized, "401 Unauthorized", []byte(unauthorizedBody)) + require.EqualError(t, err, "not authorized: your session is invalid or has expired, please run 'ziti edge login' again (401 Unauthorized)") + require.NotContains(t, err.Error(), "requestId") + require.NotContains(t, err.Error(), "apiVersion") + }) + + t.Run("403 reports a permission problem, not an expired session", func(t *testing.T) { + err := controllerResponseErrorFromParts("deleting identity", http.StatusForbidden, "403 Forbidden", nil) + require.EqualError(t, err, "not authorized: this identity does not have permission to perform this action (403 Forbidden)") + }) + + t.Run("other errors surface the api error code and message, not the raw body", func(t *testing.T) { + body := `{"error": {"code": "NOT_FOUND", "message": "resource not found"}}` + err := controllerResponseErrorFromParts("getting identity", http.StatusNotFound, "404 Not Found", []byte(body)) + require.EqualError(t, err, "error getting identity. Status code: 404 Not Found, NOT_FOUND - resource not found") + }) + + t.Run("unparsable body falls back to status only", func(t *testing.T) { + err := controllerResponseErrorFromParts("creating identity", http.StatusInternalServerError, "500 Internal Server Error", []byte("boom")) + require.EqualError(t, err, "error creating identity. Status code: 500 Internal Server Error") + }) +} + +func TestParseApiErrorMessage(t *testing.T) { + t.Run("valid envelope", func(t *testing.T) { + require.Equal(t, "NOT_FOUND - missing", parseApiErrorMessage([]byte(`{"error":{"code":"NOT_FOUND","message":"missing"}}`))) + }) + t.Run("no error field", func(t *testing.T) { + require.Equal(t, "", parseApiErrorMessage([]byte(`{"data":{}}`))) + }) + t.Run("not json", func(t *testing.T) { + require.Equal(t, "", parseApiErrorMessage([]byte("nope"))) + }) +} diff --git a/zititest/go.mod b/zititest/go.mod index dbd8456a0..4778e1639 100644 --- a/zititest/go.mod +++ b/zititest/go.mod @@ -13,12 +13,12 @@ replace github.com/michaelquigley/pfxlog => github.com/michaelquigley/pfxlog v0. require ( github.com/Jeffail/gabs v1.4.0 github.com/Jeffail/gabs/v2 v2.7.0 - github.com/go-openapi/runtime v0.32.4 + github.com/go-openapi/runtime v0.33.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/michaelquigley/pfxlog v1.0.0 github.com/openziti/agent v1.0.33 - github.com/openziti/channel/v4 v4.3.11 + github.com/openziti/channel/v4 v4.3.12 github.com/openziti/edge-api v0.31.0 github.com/openziti/fablab v0.6.16 github.com/openziti/foundation/v2 v2.0.91 @@ -30,22 +30,22 @@ require ( github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.1 github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 go.etcd.io/bbolt v1.5.0 - golang.org/x/net v0.56.0 - google.golang.org/protobuf v1.36.11 + golang.org/x/net v0.58.0 + google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/AppsFlyer/go-sundheit v0.6.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 // indirect - github.com/Azure/go-amqp v1.6.0 // indirect + github.com/Azure/go-amqp v1.7.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/MichaelMure/go-term-text v0.3.1 // indirect github.com/alecthomas/chroma v0.10.0 // indirect @@ -89,7 +89,6 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 // indirect github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect @@ -98,37 +97,37 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.19.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect - github.com/gaissmai/extnetip v1.3.1 // indirect + github.com/gaissmai/extnetip v1.3.2 // indirect github.com/go-acme/lego/v4 v4.35.2 // indirect - github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/go-chi/chi/v5 v5.3.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.25.2 // indirect + github.com/go-openapi/analysis v0.26.0 // indirect github.com/go-openapi/errors v0.22.8 // indirect - github.com/go-openapi/jsonpointer v0.24.0 // indirect - github.com/go-openapi/jsonreference v0.21.6 // indirect - github.com/go-openapi/loads v0.24.0 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect + github.com/go-openapi/loads v0.25.1 // indirect github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect - github.com/go-openapi/spec v0.22.6 // indirect - github.com/go-openapi/strfmt v0.26.4 // indirect - github.com/go-openapi/swag v0.27.0 // indirect - github.com/go-openapi/swag/cmdutils v0.27.0 // indirect - github.com/go-openapi/swag/conv v0.27.0 // indirect - github.com/go-openapi/swag/fileutils v0.27.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.1 // indirect - github.com/go-openapi/swag/jsonutils v0.27.0 // indirect - github.com/go-openapi/swag/loading v0.27.0 // indirect - github.com/go-openapi/swag/mangling v0.27.0 // indirect - github.com/go-openapi/swag/netutils v0.27.0 // indirect - github.com/go-openapi/swag/stringutils v0.27.0 // indirect - github.com/go-openapi/swag/typeutils v0.27.0 // indirect - github.com/go-openapi/swag/yamlutils v0.27.0 // indirect - github.com/go-openapi/validate v0.26.0 // indirect + github.com/go-openapi/spec v0.22.9 // indirect + github.com/go-openapi/strfmt v0.27.0 // indirect + github.com/go-openapi/swag v0.29.0 // indirect + github.com/go-openapi/swag/cmdutils v0.29.0 // indirect + github.com/go-openapi/swag/conv v0.29.0 // indirect + github.com/go-openapi/swag/fileutils v0.29.0 // indirect + github.com/go-openapi/swag/jsonutils v0.29.0 // indirect + github.com/go-openapi/swag/loading v0.29.0 // indirect + github.com/go-openapi/swag/mangling v0.29.0 // indirect + github.com/go-openapi/swag/netutils v0.29.0 // indirect + github.com/go-openapi/swag/pools v0.29.0 // indirect + github.com/go-openapi/swag/stringutils v0.29.0 // indirect + github.com/go-openapi/swag/typeutils v0.29.0 // indirect + github.com/go-openapi/swag/yamlutils v0.29.0 // indirect + github.com/go-openapi/validate v0.26.3 // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect @@ -139,7 +138,7 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-metrics v0.6.1 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -149,7 +148,7 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect - github.com/jedib0t/go-pretty/v6 v6.8.1 // indirect + github.com/jedib0t/go-pretty/v6 v6.8.3 // indirect github.com/jellydator/ttlcache/v3 v3.4.1 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jinzhu/copier v0.4.0 // indirect @@ -161,16 +160,16 @@ require ( github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lucsky/cuid v1.2.1 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-runewidth v0.0.28 // indirect github.com/mattn/go-tty v0.0.8 // indirect github.com/mdlayher/netlink v1.11.2 // indirect - github.com/mdlayher/socket v0.6.0 // indirect + github.com/mdlayher/socket v0.6.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/michaelquigley/figlet v0.1.0 // indirect - github.com/miekg/dns v1.1.72 // indirect + github.com/miekg/dns v1.1.73 // indirect github.com/miekg/pkcs11 v1.1.2 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -182,7 +181,7 @@ require ( github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/oliveagle/jsonpath v0.1.4 // indirect github.com/openziti/cobra-to-md v1.0.1 // indirect github.com/openziti/go-term-markdown v1.0.1 // indirect @@ -192,15 +191,14 @@ require ( github.com/openziti/x509-claims v1.0.3 // indirect github.com/openziti/xweb/v3 v3.0.4 // indirect github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pkg/sftp v1.13.10 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/rabbitmq/amqp091-go v1.12.0 // indirect + github.com/rabbitmq/amqp091-go v1.14.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rodaine/table v1.3.1 // indirect github.com/rs/cors v1.11.1 // indirect @@ -226,8 +224,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zitadel/logging v0.7.0 // indirect - github.com/zitadel/oidc/v3 v3.47.5 // indirect + github.com/zitadel/oidc/v3 v3.49.2 // indirect github.com/zitadel/schema v1.3.2 // indirect go.mozilla.org/pkcs7 v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -235,17 +232,15 @@ require ( go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/atomic v1.11.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect - golang.org/x/tools v0.46.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect gopkg.in/AlecAivazis/survey.v1 v1.8.8 // indirect gopkg.in/resty.v1 v1.12.0 // indirect nhooyr.io/websocket v1.8.17 // indirect diff --git a/zititest/go.sum b/zititest/go.sum index ad47cd8ea..92b4f6978 100644 --- a/zititest/go.sum +++ b/zititest/go.sum @@ -41,16 +41,16 @@ github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliG github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk= github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 h1:kE5kpeiSqu4jcCQ/sWuyggMXJ/pT6oQ99+8hwPmyeJ0= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0/go.mod h1:IAN3Z0DMtehoxoQQnfqg1891z1P7GNoDryKtFcAyMBI= -github.com/Azure/go-amqp v1.6.0 h1:pMnBstxSd2JnvTopR/L9MUdQi4e5Mp9FscP4kZ0rZ8M= -github.com/Azure/go-amqp v1.6.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-amqp v1.7.0 h1:9VlH/LEWr386XWWJRNON0eslFqSClYBXP4HewvIqkDQ= +github.com/Azure/go-amqp v1.7.0/go.mod h1:pCJaHsvRlmmFUpxyQbh2qPkUFqYJeRBTqJSHKJadvPg= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -75,7 +75,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -187,8 +186,6 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 h1:fctNkSsavbXpt8geFWZb8n+noCqS8MrOXRJ/YfdZ2dQ= @@ -215,8 +212,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -227,13 +224,13 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gaissmai/extnetip v1.3.1 h1:e8xa3hJG/+6NevjZXVkC60WBLypNjH/ugEy4kaPoKAU= -github.com/gaissmai/extnetip v1.3.1/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= +github.com/gaissmai/extnetip v1.3.2 h1:lNjdWx0pT6hXSfp4lc6jG1WPne0/PS2T8kRPYw26NsM= +github.com/gaissmai/extnetip v1.3.2/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU= github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -241,68 +238,66 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= -github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= +github.com/go-openapi/analysis v0.26.0 h1:1xECln1iMMmQnTjgcknC1vi1hA4KISt6IHpSwnqcuwI= +github.com/go-openapi/analysis v0.26.0/go.mod h1:40gERFi/2dyXA1FaqRRLxkv1IlC6X+GPDNd1xrYAjZE= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= -github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= -github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= -github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= -github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4= -github.com/go-openapi/runtime v0.32.4 h1:8ElGj/3goG0itt0nBPP6Cm57ehcYyuHoI3O20nxgvkw= -github.com/go-openapi/runtime v0.32.4/go.mod h1:Bz6keOZw1NX4T6f+m42OoT1MBPDt6Re13dbccHyGH/4= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/loads v0.25.1 h1:toKQdIDLxlqfKLLGUUmUsiTd5/X0Chzvde9EGYQP/Ac= +github.com/go-openapi/loads v0.25.1/go.mod h1:33Hen4tsKXHL45TyYojvfD5fZUFN4O1y4r/XhsRW2zc= +github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU= +github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw= github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= -github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= -github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= -github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= -github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= -github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE= -github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0= -github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= -github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= -github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= -github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8= -github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= -github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= -github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= -github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc= -github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= -github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= -github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= -github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= -github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= -github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= +github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= +github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= +github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= +github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= +github.com/go-openapi/swag v0.29.0 h1:vJ8m3Xv7L4VfGgr8nK77CHTQRg2nyFPXySpV0w3xPB8= +github.com/go-openapi/swag v0.29.0/go.mod h1:8FrS8OFgntDRBzpHD7SyqDJTmTDPZo8Kvv0OuQW+Mr4= +github.com/go-openapi/swag/cmdutils v0.29.0 h1:AKt8Q7ZfR2NmnSJD7B1WcnTOfuXHBvLPJc3W65SCM3Y= +github.com/go-openapi/swag/cmdutils v0.29.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.29.0 h1:4+1TogWpOIzMPzVKrvx1BfqBYlApB7D7DW3EAWpwmp4= +github.com/go-openapi/swag/conv v0.29.0/go.mod h1:ch1l7V87F6zQXuLs5s0RFvrro6aFvrVcfVXn2PTZnu8= +github.com/go-openapi/swag/fileutils v0.29.0 h1:meobnn3MsAkF6XmJn6qw3hPeMAOhwf7XD5BOKeFzqWU= +github.com/go-openapi/swag/fileutils v0.29.0/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI= +github.com/go-openapi/swag/jsonutils v0.29.0 h1:Xgnf9g32ycQjQUnDxkhqLraH2FhitcSE3w7ayQB3TgA= +github.com/go-openapi/swag/jsonutils v0.29.0/go.mod h1:5WYmjf6hJcBve+ArzBaUsYy4M1GXsgjIQTmwJKfZHrA= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0 h1:bpSF6LFkJJVtaRtJCzbZADVPVHQYKPwPdKthOQA2/5o= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg= +github.com/go-openapi/swag/loading v0.29.0 h1:r1lg2DQbT1VgBwgiPYXBM059RNswFI6r36CC0QCcRGw= +github.com/go-openapi/swag/loading v0.29.0/go.mod h1:l/Z4MNbom0jSqzvWJqK2VUUWEceBknGEuVbLHLq4KN0= +github.com/go-openapi/swag/mangling v0.29.0 h1:RVKyucZ2rvA/M/sqxuNZGW8Mf0+1qypVX8n2GwV11lM= +github.com/go-openapi/swag/mangling v0.29.0/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s= +github.com/go-openapi/swag/netutils v0.29.0 h1:2Y9tiqzdzRf++Tf5SGVK4Rk1iop2mo1+W3fNdcflMCo= +github.com/go-openapi/swag/netutils v0.29.0/go.mod h1:DUde7x4Bx00k5jYl2AdRpNAO0m7atUvD2x6X+bWkbno= +github.com/go-openapi/swag/pools v0.29.0 h1:uMQcoJeHJ8fWkdfEXJZMMpqk6hpfW8qTL5Q/IoRFFII= +github.com/go-openapi/swag/pools v0.29.0/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= +github.com/go-openapi/swag/stringutils v0.29.0 h1:/IEOuZ7PGJi6lqgH83dVt7/A9eHsDGEH1459lm+gpEo= +github.com/go-openapi/swag/stringutils v0.29.0/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= +github.com/go-openapi/swag/typeutils v0.29.0 h1:HrWCYZeXVVNDo/7QQPRaYk33XeIDxksbxpalID3bWR8= +github.com/go-openapi/swag/typeutils v0.29.0/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= +github.com/go-openapi/swag/yamlutils v0.29.0 h1:JOKKuhMnBx4HYTM+kPEYw8S5YKKU9PnC4Mwb+c69BBA= +github.com/go-openapi/swag/yamlutils v0.29.0/go.mod h1:/+FVozjFWZzku6mRz5U/Qmq5Yk8PLFxBLLWA/jHaxYE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= +github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= +github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.3 h1:OkfZgLvLDnGP2hrRGD+42WBiPWWkoHomTJ+IVI+KaDc= +github.com/go-openapi/validate v0.26.3/go.mod h1:7DOOa4raU6NRe7A8VQSKbm3VcuUIioREYHFt+er9Sk8= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -404,8 +399,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= -github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-metrics v0.6.1 h1:V7j9cgHTXl4OebnX3y0l6ZSoO5dTp0VI0K8Y7JfRsS4= +github.com/hashicorp/go-metrics v0.6.1/go.mod h1:XOozbQKeJz12GG8cCcQb/E5vNVeTdJt0aHbM+uHnL1M= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -452,8 +447,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= -github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= @@ -464,10 +459,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -476,7 +469,6 @@ github.com/judedaryl/go-arrayutils v0.0.1 h1:89rWXRVp1c1gcE1UEWvFuohVMeYwfA0y4TM github.com/judedaryl/go-arrayutils v0.0.1/go.mod h1:vqtnlEkOBpDGHS3U3kQtMJZGTOC+SBFAQYj2KcxLf1A= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -484,7 +476,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -517,8 +508,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -526,23 +517,23 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= +github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.8 h1:yxtc0Ye17/1ne/bjy993YUoyP8bJJFa9n5M9XTdwoZQ= github.com/mattn/go-tty v0.0.8/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= -github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= -github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -551,8 +542,8 @@ github.com/michaelquigley/figlet v0.1.0/go.mod h1:o01j/eykadr5SKE51qSYuvQ1n9Aftz github.com/michaelquigley/pfxlog v0.6.10 h1:IbC/H3MmSDcPlQHF1UZPQU13Dkrs0+ycWRyQd2ihnjw= github.com/michaelquigley/pfxlog v0.6.10/go.mod h1:gEiNTfKEX6cJHSwRpOuqBpc8oYrlhMiDK/xMk/gV7D0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -582,7 +573,6 @@ github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce h1:TqjP/BTDrwN7zP9xyXVuLsMBXYMt6LLYi55PlrIcq8U= @@ -593,8 +583,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/oliveagle/jsonpath v0.1.4 h1:Sr/ffH5YSyQKjSNfvDFkQqAqh3kn/QxF/7j2jjpfOAI= github.com/oliveagle/jsonpath v0.1.4/go.mod h1:diWEHhuLqib29heQcHYHyaLcxFC3KpKa/5ihkZBs1Z8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -609,8 +599,8 @@ github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/openziti/agent v1.0.33 h1:W6LXs+dWIzg/V8KNJq501zUxFSjtDQTaBFdqOcUFCTg= github.com/openziti/agent v1.0.33/go.mod h1:pjQ9jSOl+9ZR/0Y9+wOwJhGekYwyai2uKB3YCd0nulI= -github.com/openziti/channel/v4 v4.3.11 h1:ugezUhTEuSQnVFKUioN1gh6f+ZF4eBqx6mOrT5DrY5U= -github.com/openziti/channel/v4 v4.3.11/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= +github.com/openziti/channel/v4 v4.3.12 h1:kpox7z7aILokmzejUlOobORzQutH6q3t4E5abuZcOPA= +github.com/openziti/channel/v4 v4.3.12/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQfVXxE= github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c= github.com/openziti/edge-api v0.31.0 h1:QdaaPnKQj4B40q404+c8VZxMsoVpfGpfOhzs3LLCd/A= @@ -648,8 +638,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -668,31 +658,23 @@ github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1Hbe github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rabbitmq/amqp091-go v1.12.0 h1:V0v14Iqfs+MwHWihJt/nGS5Ulu0vw572b2Co3mwunkI= -github.com/rabbitmq/amqp091-go v1.12.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rabbitmq/amqp091-go v1.14.0 h1:RSaT7aOKt/OrkVUyswPDW29lnRz9psuGmfZFBmLqLek= +github.com/rabbitmq/amqp091-go v1.14.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -724,10 +706,9 @@ github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsB github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= +github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -765,8 +746,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -800,10 +781,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= -github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= -github.com/zitadel/oidc/v3 v3.47.5 h1:cR2z0oqa5XZkwpXQiPCUGqKtndrjHgEXb81y3oXocK4= -github.com/zitadel/oidc/v3 v3.47.5/go.mod h1:XxFh0666HRXycyrKmono+3gY0RACpYJLgy4r/+kliKY= +github.com/zitadel/oidc/v3 v3.49.2 h1:yKvB2Hx6rVWp0vOTk1pEyXAPxJusgIHNmr20AxOohmQ= +github.com/zitadel/oidc/v3 v3.49.2/go.mod h1:HwoguOGo0eem0RK5Gb+P6Q4aQLVinJ9LhomlVEA57ck= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= @@ -837,8 +816,9 @@ 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.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -852,8 +832,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -864,8 +844,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -892,8 +872,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -938,8 +916,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -966,8 +944,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -994,7 +972,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1008,8 +985,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1021,7 +996,6 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1029,7 +1003,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1038,13 +1011,13 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1054,8 +1027,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1114,8 +1087,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1222,8 +1193,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY= gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=